Failure, with context.
Environment
- System
- linux 6.18.44 / x64
- Directory
- /tmp/reproshot-demo-GezJVt
- node
- 24.19.0
- npm
- 11.9.0
- pnpm
- 11.19.0
- python
- 3.12.14
- pip
- 26.2.1
- java
- 17.0.20
Source state
- Commit
- ab9e455f17d3754be6f3d65a26e7b5683358b801
- Branch
- main
- Working tree
- Clean
- Patch
- none
- Captured
- 2026-09-13T13:20:27.722Z
- Signal
- None
Reproduce
Captured on linux x64. Versions: node 24.19.0, npm 11.9.0, pnpm 11.19.0, python 3.12.14, pip 26.2.1, java 17.0.20. +:root{color-scheme:dark light;--bg:#0d1117;--panel:#161b22;--line:#30363d;--text:#e6edf3;--muted:#8b949e;--accent:#58a6ff;--red:#f85149;--green:#3fb950;--amber:#d29922}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.6 system-ui,sans-serif}main{max-width:960px;margin:0 auto;padding:36px 24px}header{border-bottom:1px solid var(--line);padding-bottom:18px}.brand{font:600 14px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);letter-spacing:.08em;text-transform:uppercase}h1{font-size:28px;line-height:1.25;margin:6px 0 0}.status-failure,.status-spawn-error{color:var(--red)}.status-success{color:var(--green)}.status-interrupted{color:var(--amber)}h2{font-size:18px;margin:28px 0 12px}.muted,small{color:var(--muted)}.command,pre{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--panel);border:1px solid var(--line);padding:16px;border-radius:5px;font:13px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace}.command{font-size:16px;border-left:3px solid var(--accent);margin:22px 0}.command::before{content:'$ ';color:var(--accent)}.facts{display:grid;grid-template-columns:repeat(5,1fr);border-block:1px solid var(--line)}.fact{padding:13px 14px;border-right:1px solid var(--line)}.fact:first-child{padding-left:0}.fact:last-child{border:0}.fact small{display:block}.fact strong{font:500 13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}.grid{display:grid;grid-template-columns:1fr 1fr;gap:32px}dl{display:grid;grid-template-columns:105px minmax(0,1fr);gap:8px 14px;font-size:14px}dt{color:var(--muted)}dd{margin:0;overflow-wrap:anywhere}a{color:var(--accent)}ul{padding-left:20px}.files{display:flex;flex-wrap:wrap;gap:8px;padding:0;list-style:none}.files a{display:block;padding:6px 10px;border:1px solid var(--line);border-radius:4px;text-decoration:none;font:12px ui-monospace,SFMono-Regular,Consolas,monospace}footer{border-top:1px solid var(--line);margin-top:30px;padding-top:18px;font-size:12px;color:var(--muted)}summary{cursor:pointer;font-weight:600}details{margin-top:22px}pre{max-height:520px;overflow:auto}.notice{color:var(--muted);font-size:13px}@media(max-width:720px){main{padding:28px 18px}.grid{grid-template-columns:1fr;gap:0}.facts{grid-template-columns:1fr 1fr}.fact,.fact:first-child{padding:12px;border-bottom:1px solid var(--line)}}@media(prefers-color-scheme:light){:root{--bg:#f6f8fa;--panel:#fff;--line:#d0d7de;--text:#1f2328;--muted:#59636e;--accent:#0969da;--red:#cf222e;--green:#1a7f37;--amber:#9a6700}} +
Capture failed
Environment
- System
- linux 6.18.44 / x64
- Directory
- /tmp/reproshot-demo-GezJVt
- node
- 24.19.0
- npm
- 11.9.0
- pnpm
- 11.19.0
- python
- 3.12.14
- pip
- 26.2.1
- java
- 17.0.20
Source state
- Commit
- ab9e455f17d3754be6f3d65a26e7b5683358b801
- Branch
- main
- Working tree
- Clean
- Patch
- none
- Captured
- 2026-09-13T13:20:27.722Z
- Signal
- None
Reproduce
Captured on linux x64. Versions: node 24.19.0, npm 11.9.0, pnpm 11.19.0, python 3.12.14, pip 26.2.1, java 17.0.20. Use the project working directory (/tmp/reproshot-demo-GezJVt). Restore source revision ab9e455f17d3754be6f3d65a26e7b5683358b801, review any patch, and prepare dependencies yourself. Environment variable values were not collected. Review the command before running.
'npm' 'test'
diff --git a/examples/generated/reproshot.svg b/examples/generated/reproshot.svg
index c0e3328..ecdaf0b 100644
--- a/examples/generated/reproshot.svg
+++ b/examples/generated/reproshot.svg
@@ -1 +1 @@
-
+
diff --git a/src/report.ts b/src/report.ts
index d51dc91..97fe268 100644
--- a/src/report.ts
+++ b/src/report.ts
@@ -98,6 +98,67 @@ export function issueBody(m: Manifest, stdout: string, stderr: string): string {
'## Expected behavior\n\n\n\n## Additional context\n\n\n\nReproShot redacted detected secrets. Review this report before posting.\n',
);
}
+
+const statusText = (status: Manifest['status']): string =>
+ ({
+ success: 'capture succeeded',
+ failure: 'capture failed',
+ interrupted: 'capture interrupted',
+ 'spawn-error': 'spawn error',
+ })[status];
+
+const statusHeading = (status: Manifest['status']): string => {
+ const value = statusText(status);
+ return value[0]!.toUpperCase() + value.slice(1);
+};
+
+const compactLine = (value: unknown, limit: number): string => {
+ const chars = [...String(value).replace(/\s+/gu, ' ').trim()];
+ return chars.length <= limit ? chars.join('') : `${chars.slice(0, limit - 1).join('')}…`;
+};
+
+const resultSummary = (m: Manifest): string => {
+ const result =
+ m.status === 'spawn-error'
+ ? 'not started'
+ : m.result.signal
+ ? `signal ${compactLine(m.result.signal, 18)}`
+ : m.result.exitCode === null
+ ? 'exit unavailable'
+ : `exit ${m.result.exitCode}`;
+ return `${statusText(m.status)} · ${result} · ${(m.result.durationMs / 1000).toFixed(2)}s`;
+};
+
+const runtimeSummary = (m: Manifest): string => {
+ const priority = (name: string) => (name === 'node' ? 0 : name === 'npm' ? 1 : 2);
+ const entries = Object.entries(m.environment.runtimes).sort(([a], [b]) => {
+ const rank = priority(a) - priority(b);
+ return rank || (a < b ? -1 : a > b ? 1 : 0);
+ });
+ if (!entries.length) return 'not detected';
+ const visible = entries
+ .slice(0, 2)
+ .map(([name, version]) => `${compactLine(name, 10)} ${compactLine(version, 16)}`);
+ if (entries.length > visible.length) visible.push(`+${entries.length - visible.length}`);
+ return visible.join(' · ');
+};
+
+const gitSummary = (m: Manifest): string => {
+ if (!m.git.available) return 'unavailable';
+ const commit = m.git.commit ? compactLine(m.git.commit.slice(0, 7), 7) : 'no commit';
+ const branch = m.git.branch ? compactLine(m.git.branch, 24) : 'detached';
+ const tree = m.git.dirty === null ? 'unknown' : m.git.dirty ? 'modified' : 'clean';
+ return `${commit} · ${branch} · ${tree}`;
+};
+
+const logsSummary = (m: Manifest): string =>
+ `stdout ${m.logs.stdout.truncated ? 'truncated' : 'captured'} · stderr ${m.logs.stderr.truncated ? 'truncated' : 'captured'}`;
+
+const replaySummary = (m: Manifest): string => {
+ if (!m.reproduction.scripts) return 'not available';
+ return `reproduce.sh · reproduce.ps1${m.reproduction.requiresEditing ? ' · edit required' : ''}`;
+};
+
export function html(m: Manifest, stdout: string, stderr: string): string {
const e = escapeHtml;
const facts = [
@@ -105,11 +166,12 @@ export function html(m: Manifest, stdout: string, stderr: string): string {
['Duration', `${(m.result.durationMs / 1000).toFixed(2)}s`],
['Commit', m.git.commit?.slice(0, 7) ?? 'unavailable'],
['Secrets', `${m.redaction.detectedSecrets} redacted`],
+ ['Repro Score', `${m.score.total}/100`],
];
return `
ReproShot · ${e(m.status)} REPROSHOT / LOCAL CAPTURE${m.status === 'failure' ? 'Failure, with context.' : m.status === 'success' ? 'Command captured.' : 'Capture ' + e(m.status) + '.'}
One command. A report you can share.${m.score.total} / 100REPRO SCORE ${e(m.command.display)}${facts.map(([k, v]) => `${k}${e(String(v))}`).join('')} Environment
- System
- ${e(m.environment.os + ' ' + m.environment.release + ' / ' + m.environment.arch)}
- Directory
- ${e(m.environment.cwd)}
${Object.entries(
+:root{color-scheme:dark light;--bg:#0d1117;--panel:#161b22;--line:#30363d;--text:#e6edf3;--muted:#8b949e;--accent:#58a6ff;--red:#f85149;--green:#3fb950;--amber:#d29922}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.6 system-ui,sans-serif}main{max-width:960px;margin:0 auto;padding:36px 24px}header{border-bottom:1px solid var(--line);padding-bottom:18px}.brand{font:600 14px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);letter-spacing:.08em;text-transform:uppercase}h1{font-size:28px;line-height:1.25;margin:6px 0 0}.status-failure,.status-spawn-error{color:var(--red)}.status-success{color:var(--green)}.status-interrupted{color:var(--amber)}h2{font-size:18px;margin:28px 0 12px}.muted,small{color:var(--muted)}.command,pre{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--panel);border:1px solid var(--line);padding:16px;border-radius:5px;font:13px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace}.command{font-size:16px;border-left:3px solid var(--accent);margin:22px 0}.command::before{content:'$ ';color:var(--accent)}.facts{display:grid;grid-template-columns:repeat(5,1fr);border-block:1px solid var(--line)}.fact{padding:13px 14px;border-right:1px solid var(--line)}.fact:first-child{padding-left:0}.fact:last-child{border:0}.fact small{display:block}.fact strong{font:500 13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}.grid{display:grid;grid-template-columns:1fr 1fr;gap:32px}dl{display:grid;grid-template-columns:105px minmax(0,1fr);gap:8px 14px;font-size:14px}dt{color:var(--muted)}dd{margin:0;overflow-wrap:anywhere}a{color:var(--accent)}ul{padding-left:20px}.files{display:flex;flex-wrap:wrap;gap:8px;padding:0;list-style:none}.files a{display:block;padding:6px 10px;border:1px solid var(--line);border-radius:4px;text-decoration:none;font:12px ui-monospace,SFMono-Regular,Consolas,monospace}footer{border-top:1px solid var(--line);margin-top:30px;padding-top:18px;font-size:12px;color:var(--muted)}summary{cursor:pointer;font-weight:600}details{margin-top:22px}pre{max-height:520px;overflow:auto}.notice{color:var(--muted);font-size:13px}@media(max-width:720px){main{padding:28px 18px}.grid{grid-template-columns:1fr;gap:0}.facts{grid-template-columns:1fr 1fr}.fact,.fact:first-child{padding:12px;border-bottom:1px solid var(--line)}}@media(prefers-color-scheme:light){:root{--bg:#f6f8fa;--panel:#fff;--line:#d0d7de;--text:#1f2328;--muted:#59636e;--accent:#0969da;--red:#cf222e;--green:#1a7f37;--amber:#9a6700}}
+ReproShot${e(statusHeading(m.status))}
${e(m.command.display)}${facts.map(([k, v]) => `${k}${e(String(v))}`).join('')} Environment
- System
- ${e(m.environment.os + ' ' + m.environment.release + ' / ' + m.environment.arch)}
- Directory
- ${e(m.environment.cwd)}
${Object.entries(
m.environment.runtimes,
)
.map(([k, v]) => `- ${e(k)}
- ${e(v)}
`)
@@ -118,6 +180,15 @@ export function html(m: Manifest, stdout: string, stderr: string): string {
)}
Source state
- Commit
- ${e(m.git.commit ?? 'Not recorded')}
- Branch
- ${e(m.git.branch ?? 'Detached or unavailable')}
- Working tree
- ${m.git.dirty === null ? 'Unknown' : m.git.dirty ? 'Modified' : 'Clean'}
- Patch
- ${e(m.git.patch)}
- Captured
- ${e(m.timestamp)}
- Signal
- ${e(m.result.signal ?? 'None')}
Reproduce
${e(requirements(m))}
${m.reproduction.requiresEditing ? 'Helpers stop until sanitized or shortened arguments are restored.
' : ''}${e(renderSh(m.command.argv))}\n\n# PowerShell\n${e(renderPowerShell(m.command.argv))}stderr ${m.logs.stderr.truncated ? '· truncated' : ''}
${e(preview(stderr) || '(empty)')}stdout ${m.logs.stdout.truncated ? '· truncated' : ''}
${e(preview(stdout) || '(empty)')}Bundle files
${m.files.map((f) => `- ${e(f)}
`).join('')}
Evidence behind the score
${m.score.items.map((i) => `- ${e(i.name)}: ${i.points}/${i.max} — ${e(i.detail)}
`).join('')}
Capture notes
${[m.redaction.notice, ...m.warnings, ...m.git.notes].map((x) => `- ${e(x)}
`).join('')}
\n`;
}
export function svg(m: Manifest): string {
- const rows = m.score.items;
- return `\n`;
+ const accent =
+ m.status === 'success' ? '#3fb950' : m.status === 'interrupted' ? '#d29922' : '#f85149';
+ const lines = [
+ ['repro score', `${m.score.total}/100`],
+ ['git', gitSummary(m)],
+ ['runtime', runtimeSummary(m)],
+ ['logs', logsSummary(m)],
+ ['replay', replaySummary(m)],
+ ['secrets redacted', m.redaction.detectedSecrets],
+ ];
+ return `\n`;
}
diff --git a/test/report.test.mjs b/test/report.test.mjs
index d8b81ec..cc6d4ba 100644
--- a/test/report.test.mjs
+++ b/test/report.test.mjs
@@ -108,6 +108,11 @@ test('reports are deterministic and HTML escapes hostile log and metadata', () =
assert.doesNotMatch(html(m, log, ''), / npm test';
+ m.git.branch = '';
+ m.environment.runtimes = {
+ python: '3.12.0',
+ npm: '10.0.0',
+ cargo: '1.80.0',
+ node: '22.0.0',
+ };
+ const image = svg(m);
+ assert.doesNotMatch(image, /