@giamme/forge 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +106 -1766
  3. package/SKILL.md +3 -2
  4. package/VERSION +1 -1
  5. package/docs/README.md +26 -0
  6. package/docs/add-ons.md +33 -0
  7. package/docs/artifacts.md +69 -0
  8. package/docs/commands.md +315 -0
  9. package/docs/dataset.md +20 -0
  10. package/docs/decomposed-runs.md +268 -0
  11. package/docs/development.md +95 -0
  12. package/docs/forge-ui.md +92 -0
  13. package/docs/fractal.md +181 -0
  14. package/docs/install.md +160 -0
  15. package/docs/memory.md +156 -0
  16. package/docs/models.md +118 -0
  17. package/docs/reviews.md +231 -0
  18. package/docs/safety.md +55 -0
  19. package/docs/troubleshooting.md +54 -0
  20. package/forge +5 -0
  21. package/package.json +3 -2
  22. package/references/decompose.md +1 -1
  23. package/references/fractal.md +1 -1
  24. package/references/harnesses.md +18 -6
  25. package/references/solo.md +1 -1
  26. package/scripts/forge-dispatch.sh +7 -1
  27. package/scripts/forge-runs.py +121 -0
  28. package/scripts/forge-runtime.py +59 -6
  29. package/scripts/forge-version.py +54 -12
  30. package/scripts/forge_dataset/harvest.py +1 -1
  31. package/scripts/forge_dataset/importer.py +1 -1
  32. package/scripts/forge_fractal/__init__.py +2 -1
  33. package/ui/README.md +80 -20
  34. package/ui/THIRD_PARTY_NOTICES.md +108 -0
  35. package/ui/docs/api.md +99 -17
  36. package/ui/forge_ui/export.py +16 -6
  37. package/ui/forge_ui/fractal.py +1 -2
  38. package/ui/forge_ui/reader.py +95 -7
  39. package/ui/forge_ui/server.py +10 -1
  40. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  41. package/ui/forge_ui/static/app.js +279 -124
  42. package/ui/forge_ui/static/cloud3d.js +3 -2
  43. package/ui/forge_ui/static/cloudui.js +8 -5
  44. package/ui/forge_ui/static/convo.js +295 -0
  45. package/ui/forge_ui/static/datatable.js +232 -0
  46. package/ui/forge_ui/static/diffview.js +29 -10
  47. package/ui/forge_ui/static/findings3d.js +134 -0
  48. package/ui/forge_ui/static/graph.js +3 -3
  49. package/ui/forge_ui/static/icons.js +94 -0
  50. package/ui/forge_ui/static/index.html +25 -13
  51. package/ui/forge_ui/static/iso.js +70 -19
  52. package/ui/forge_ui/static/leaderboard.js +44 -48
  53. package/ui/forge_ui/static/panel.js +68 -32
  54. package/ui/forge_ui/static/runmodel.js +17 -1
  55. package/ui/forge_ui/static/scene3d.js +518 -43
  56. package/ui/forge_ui/static/search.js +2 -1
  57. package/ui/forge_ui/static/shell.js +137 -0
  58. package/ui/forge_ui/static/stagectl.js +150 -0
  59. package/ui/forge_ui/static/style.css +924 -469
  60. package/ui/forge_ui/static/theme.js +25 -0
  61. package/ui/forge_ui/transcripts.py +608 -0
@@ -63,8 +63,9 @@ window.ForgeLeaderboard = (() => {
63
63
  function modelCell(alias, ctx, few) {
64
64
  const td = el('td', undefined, 'lb-model');
65
65
  const href = ctx.modelHref ? ctx.modelHref(alias) : null;
66
- if (href) { const a = el('a', alias, 'mono lb-alias'); a.href = href; a.title = 'show its runs in the cloud'; td.append(a); }
67
- else td.append(el('span', alias, 'mono'));
66
+ const plate = window.ForgeIcons ? window.ForgeIcons.modelPlate(alias) : el('span', alias, 'mono');
67
+ if (href) { const a = el('a', undefined, 'lb-alias'); a.href = href; a.title = 'show its runs in the cloud'; a.append(plate); td.append(a); }
68
+ else td.append(plate);
68
69
  if (few) { const s = el('span', 'few', 'lb-few'); s.title = few; td.append(s); }
69
70
  return td;
70
71
  }
@@ -96,11 +97,6 @@ window.ForgeLeaderboard = (() => {
96
97
  td.title = `input ${count(t.input)} (cached ${count(t.cached)}) · output ${count(t.output)} · recorded by ${count(used)} of ${plural(total, unit)}`;
97
98
  return td;
98
99
  }
99
- function head(table, cols) {
100
- const thead = el('thead'), tr = el('tr');
101
- for (const [label, cls, title] of cols) { const th = el('th', label, cls || ''); th.scope = 'col'; if (title) th.title = title; tr.append(th); }
102
- thead.append(tr); table.append(thead);
103
- }
104
100
 
105
101
  /* ---------- implementers ---------- */
106
102
  const IMPL_COLS = [
@@ -116,31 +112,31 @@ window.ForgeLeaderboard = (() => {
116
112
  ['Median model time', 'lb-num', 'Median time in the model phase per attempt'],
117
113
  ['Tokens in / out', 'lb-num', 'Summed over the attempts that recorded tokens'],
118
114
  ];
115
+ const colsOf = (spec, cells) => spec.map(([label, cls, title], i) => ({ ...cells[i], label, cls, title }));
116
+ // a sortable, reorderable table (datatable.js); one set of preferences per kind of table, shared by its copies
117
+ function table(id, label, columns, rows, ctx, rowClass) {
118
+ const host = el('div', undefined, 'lb-dt');
119
+ window.ForgeTable.render(host, { id, label, columns, rows, className: 'lb-table', rowClass,
120
+ prefs: ctx.tablePrefs ? ctx.tablePrefs(id) : null, savePrefs: p => ctx.saveTablePrefs && ctx.saveTablePrefs(id, p) });
121
+ return host;
122
+ }
119
123
  function implementerTable(rows, ctx, minN, caption) {
120
- const wrap = el('div', undefined, 'table-scroll'), table = el('table', undefined, 'lb-table');
121
- table.append(el('caption', caption, 'visually-hidden'));
122
- head(table, IMPL_COLS);
123
- const body = el('tbody');
124
- for (const r of rows) {
125
- const tr = el('tr', undefined, r.small ? 'lb-small' : '');
126
- const reasons = Object.entries(r.first.reasons).map(([k, v]) => `${v} ${k.replace(/_/g, ' ')}`).join(', ');
127
- const tk = r.timeoutKinds;
128
- tr.append(
129
- modelCell(r.alias, ctx, r.small ? `Fewer than ${plural(minN, 'first-attempt outcome')}: too few to rank` : ''),
130
- numCell(r.tasks),
131
- rateCell(r.first),
132
- numCell(r.first.censored, reasons || undefined),
133
- numCell(r.retries),
134
- numCell(r.escalationsAway),
135
- numCell(r.timeouts, r.timeouts ? `implementer ${tk.implementer} · reviewer ${tk.reviewer} · forge ${tk.forge}` : undefined),
136
- numCell(r.gateAccepts),
137
- numCell(r.humanAccepts),
138
- numCell(dur(r.medianModelS), `median wall time per attempt ${dur(r.medianDurationS)}`),
139
- tokenCell(r.tokens, r.attempts.total, 'attempt'));
140
- body.append(tr);
141
- }
142
- table.append(body); wrap.append(table);
143
- return wrap;
124
+ const tk = r => r.timeoutKinds;
125
+ const columns = colsOf(IMPL_COLS, [
126
+ { id: 'model', value: r => r.alias, cell: r => modelCell(r.alias, ctx, r.small ? `Fewer than ${plural(minN, 'first-attempt outcome')}: too few to rank` : '') },
127
+ { id: 'tasks', value: r => r.tasks, cell: r => numCell(r.tasks) },
128
+ { id: 'pass', value: r => r.first.n ? r.first.lo : null, cell: r => rateCell(r.first) },
129
+ { id: 'censored', value: r => r.first.censored, cell: r => numCell(r.first.censored, Object.entries(r.first.reasons).map(([k, v]) => `${v} ${k.replace(/_/g, ' ')}`).join(', ') || undefined) },
130
+ { id: 'retries', value: r => r.retries, cell: r => numCell(r.retries) },
131
+ { id: 'escalated', value: r => r.escalationsAway, cell: r => numCell(r.escalationsAway) },
132
+ { id: 'timeouts', value: r => r.timeouts, cell: r => numCell(r.timeouts, r.timeouts ? `implementer ${tk(r).implementer} · reviewer ${tk(r).reviewer} · forge ${tk(r).forge}` : undefined) },
133
+ { id: 'gate', value: r => r.gateAccepts, cell: r => numCell(r.gateAccepts) },
134
+ { id: 'human', value: r => r.humanAccepts, cell: r => numCell(r.humanAccepts) },
135
+ { id: 'model-time', value: r => num(r.medianModelS), cell: r => numCell(dur(r.medianModelS), `median wall time per attempt ${dur(r.medianDurationS)}`) },
136
+ { id: 'tokens', value: r => (r.tokens.attempts ?? r.tokens.reviews) ? (r.tokens.input || 0) + (r.tokens.output || 0) : null, cell: r => tokenCell(r.tokens, r.attempts.total, 'attempt') },
137
+ ]);
138
+ columns[2].title += '. Sorts by the lower end of the interval, as the ranking does';
139
+ return table('implementers', caption, columns, rows, ctx, r => r.small ? 'lb-small' : '');
144
140
  }
145
141
 
146
142
  /* ---------- reviewers ---------- */
@@ -176,7 +172,7 @@ window.ForgeLeaderboard = (() => {
176
172
  const avg = r.findings[s];
177
173
  if (avg === null) return numCell('—', 'No review reply could be read');
178
174
  const td = numCell(avg === 0 ? 0 : avg.toFixed(avg < 10 ? 2 : 1), `${count(r.findingTotals[s])} confirmed ${s} over ${plural(r.findings.n, 'review')}`);
179
- if (s === 'P0' && avg > 0) td.classList.add('lb-p0', 'st-fail');
175
+ if (s === 'P0' && avg > 0) td.classList.add('lb-p0');
180
176
  return td;
181
177
  }
182
178
  function agreementCell(a) {
@@ -187,25 +183,25 @@ window.ForgeLeaderboard = (() => {
187
183
  return td;
188
184
  }
189
185
  function reviewerTable(rows, ctx) {
190
- const wrap = el('div', undefined, 'table-scroll'), table = el('table', undefined, 'lb-table');
191
- table.append(el('caption', 'Reviewers', 'visually-hidden'));
192
- head(table, REV_COLS);
193
- const body = el('tbody');
194
- for (const r of rows) {
195
- const tr = el('tr');
196
- tr.append(modelCell(r.alias, ctx, ''), numCell(r.reviews.primary), numCell(r.reviews.shadow), numCell(r.reviews.final), verdictCell(r.verdicts),
197
- ...MS.SEVERITIES.map(s => findingCell(r, s)), agreementCell(r.agreement), numCell(dur(r.medianDurationS)), tokenCell(r.tokens, r.reviews.total, 'review'));
198
- body.append(tr);
199
- }
200
- table.append(body); wrap.append(table);
201
- return wrap;
186
+ const columns = colsOf(REV_COLS, [
187
+ { id: 'model', value: r => r.alias, cell: r => modelCell(r.alias, ctx, '') },
188
+ { id: 'primary', value: r => r.reviews.primary, cell: r => numCell(r.reviews.primary) },
189
+ { id: 'shadow', value: r => r.reviews.shadow, cell: r => numCell(r.reviews.shadow) },
190
+ { id: 'final', value: r => r.reviews.final, cell: r => numCell(r.reviews.final) },
191
+ { id: 'verdicts', value: r => { const v = r.verdicts, t = v.PASS + v.FAIL + v.other; return t ? v.PASS / t : null; }, cell: r => verdictCell(r.verdicts) },
192
+ ...MS.SEVERITIES.map(sv => ({ id: 'sev-' + sv, value: r => r.findings[sv], cell: r => findingCell(r, sv) })),
193
+ { id: 'agreement', value: r => r.agreement.n ? r.agreement.p : null, cell: r => agreementCell(r.agreement) },
194
+ { id: 'time', value: r => num(r.medianDurationS), cell: r => numCell(dur(r.medianDurationS)) },
195
+ { id: 'tokens', value: r => (r.tokens.attempts ?? r.tokens.reviews) ? (r.tokens.input || 0) + (r.tokens.output || 0) : null, cell: r => tokenCell(r.tokens, r.reviews.total, 'review') },
196
+ ]);
197
+ return table('reviewers', 'Reviewers', columns, rows, ctx);
202
198
  }
203
199
 
204
200
  /* ---------- header, caveat and filters ---------- */
205
201
  function header(L, O, f, update, withNote) {
206
202
  const t = L.totals, frag = document.createDocumentFragment();
207
203
  const top = el('header', undefined, 'lb-head');
208
- top.append(el('h1', 'Models'), el('p', `${plural(t.runs, 'run')} · ${plural(t.tasks, 'task')} · ${plural(t.attempts, 'attempt')} · ${plural(t.reviews, 'review')} counted`, 'muted lb-summary'));
204
+ top.append(el('h1', 'Models', 'module-title'), el('p', `${plural(t.runs, 'run')} · ${plural(t.tasks, 'task')} · ${plural(t.attempts, 'attempt')} · ${plural(t.reviews, 'review')} counted`, 'muted lb-summary'));
209
205
  const note = el('div', undefined, 'lb-caveat'); note.setAttribute('role', 'note');
210
206
  const p = el('p');
211
207
  p.append(el('strong', 'Observational, not causal. '), document.createTextNode('Forge routes each task to a model by its difficulty and by how models did before, so a model sent the hard tasks looks worse here than it is. Exploratory picks, the tasks where forge drew the model at random, are the fairest comparison.'));
@@ -276,12 +272,12 @@ window.ForgeLeaderboard = (() => {
276
272
  root.append(emptyNote('No tasks or reviews match these filters.', [], reset));
277
273
  } else {
278
274
  const impl = el('section', undefined, 'lb-section');
279
- impl.append(el('h2', 'Implementers'), el('p', 'Each task counts once, for the model that ran its first attempt; retries, escalations and accepts show what happened after. Ranked by the lower end of the 95% interval, so a high rate on few tasks does not jump the table.', 'lb-lead'));
275
+ impl.append(el('h2', 'Implementers', 'plate'), el('p', 'Each task counts once, for the model that ran its first attempt; retries, escalations and accepts show what happened after. Ranked by the lower end of the 95% interval, so a high rate on few tasks does not jump the table.', 'lb-lead'));
280
276
  impl.append(L.implementers.length ? implementerTable(L.implementers, ctx, f.minN, 'Implementers') : el('p', 'No attempts with a recorded model match these filters.', 'muted'));
281
277
  root.append(impl);
282
278
  if (!f.difficulty && L.implementers.length) {
283
279
  const by = el('section', undefined, 'lb-section');
284
- by.append(el('h2', 'By difficulty'), el('p', 'The same table for the tasks of one declared difficulty: the comparison routing distorts least.', 'lb-lead'));
280
+ by.append(el('h2', 'By difficulty', 'plate'), el('p', 'The same table for the tasks of one declared difficulty: the comparison routing distorts least.', 'lb-lead'));
285
281
  for (const d of MS.DIFFS) {
286
282
  const rows = L.byDifficulty[d];
287
283
  if (!rows.length) continue;
@@ -299,7 +295,7 @@ window.ForgeLeaderboard = (() => {
299
295
  root.append(by);
300
296
  }
301
297
  const rev = el('section', undefined, 'lb-section');
302
- rev.append(el('h2', 'Reviewers'), el('p', 'Primary reviews of each attempt, shadow reviews run beside them and final reviews of a whole run. Findings are confirmed findings per review.', 'lb-lead'));
298
+ rev.append(el('h2', 'Reviewers', 'plate'), el('p', 'Primary reviews of each attempt, shadow reviews run beside them and final reviews of a whole run. Findings are confirmed findings per review.', 'lb-lead'));
303
299
  rev.append(L.reviewers.length ? reviewerTable(L.reviewers, ctx) : el('p', 'No reviews with a recorded model match these filters.', 'muted'));
304
300
  root.append(rev);
305
301
  }
@@ -9,7 +9,6 @@ window.ForgePanel = (() => {
9
9
  const display = v => v === null || v === undefined || v === '' ? '—' : typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v);
10
10
  const STATUSES = new Set(['merged', 'pass', 'running', 'pending', 'fail', 'error', 'infra', 'timeout', 'unknown', 'invalidated', 'blocked',
11
11
  'interrupted', 'nochanges', 'conflict', 'accepted', 'none']);
12
- const SEV = { P0: 'fail', P1: 'conflict', P2: 'timeout', P3: 'unknown' };
13
12
  const cache = new Map(); // path → {data, error, at}
14
13
  function el(tag, text, className) {
15
14
  const node = document.createElement(tag);
@@ -21,6 +20,18 @@ window.ForgePanel = (() => {
21
20
  const s = String(value || 'unknown').toLowerCase();
22
21
  return el('span', value || '—', 'chip st-' + (cls || (STATUSES.has(s) ? s : 'unknown')));
23
22
  }
23
+ // a finding's severity: its own shape and ramp (style.css .sev-P0…P3), never a status lamp
24
+ const plate = spec => window.ForgeIcons ? window.ForgeIcons.modelPlate(spec) : el('span', spec || '—', 'mono');
25
+ const plates = specs => { const s = el('span', undefined, 'plates'); for (const x of specs) s.append(plate(x)); return specs.length ? s : undefined; };
26
+ // a long path cut in the middle: its head gives way, its last part always shows; copying takes all of it
27
+ function midPath(v) {
28
+ const s = String(v || ''), i = s.lastIndexOf('/', s.length - 2);
29
+ if (!s) return undefined;
30
+ const out = el('span', undefined, 'mid-path'); out.title = s;
31
+ out.append(el('span', i > 0 ? s.slice(0, i + 1) : '', 'mid-head'), el('span', i > 0 ? s.slice(i + 1) : s, 'mid-tail'));
32
+ return out;
33
+ }
34
+ const sevChip = (text, sev) => el('span', text, 'chip sev-chip sev-' + (/^P[0-3]$/.test(sev) ? sev : 'none'));
24
35
  function time(v) {
25
36
  if (num(v) === null) return '—';
26
37
  const d = new Date(v * 1000); return Number.isNaN(d.getTime()) ? '—' : d.toLocaleString();
@@ -87,7 +98,7 @@ window.ForgePanel = (() => {
87
98
  const row = el('div', undefined, 'chips');
88
99
  if (!f) { row.append(el('span', 'no findings parsed', 'muted')); parent.append(row); return; }
89
100
  let any = false;
90
- for (const sev of ['P0', 'P1', 'P2', 'P3']) if (f[sev]) { row.append(chip(`${sev} × ${f[sev]}`, SEV[sev])); any = true; }
101
+ for (const sev of ['P0', 'P1', 'P2', 'P3']) if (f[sev]) { row.append(sevChip(`${sev} × ${f[sev]}`, sev)); any = true; }
91
102
  if (f.unlabelled) { row.append(chip(`unlabelled × ${f.unlabelled}`, 'unknown')); any = true; }
92
103
  if (f.plausible) { row.append(chip(`plausible × ${f.plausible}`, 'pending')); any = true; }
93
104
  if (!any) row.append(el('span', 'no confirmed findings', 'muted'));
@@ -124,7 +135,7 @@ window.ForgePanel = (() => {
124
135
  findings.slice().sort((a, b) => order(a) - order(b)).forEach((f, i) => {
125
136
  const li = el('li', undefined, 'finding' + (f.status === 'CONFIRMED' ? '' : ' plausible'));
126
137
  const head = el('div', undefined, 'finding-head');
127
- head.append(chip(f.severity || 'P?', f.severity ? SEV[f.severity] : 'unknown'), el('span', f.status, 'tag'));
138
+ head.append(sevChip(f.severity || 'P?', f.severity), el('span', f.status, 'tag'));
128
139
  if (f.location) head.append(el('code', f.location));
129
140
  li.append(head, el('p', f.summary || ''));
130
141
  if (f.text && f.text !== f.summary) { const d = el('details'); d.dataset.keep = `${key}-f${i}`; d.append(el('summary', 'Full finding'), el('pre', f.text)); li.append(d); }
@@ -159,17 +170,26 @@ window.ForgePanel = (() => {
159
170
  const tokens = t => t ? `${display(t.input)} in (${display(t.cached)} cached) · ${display(t.output)} out` : '—';
160
171
  function modelFields(m) {
161
172
  if (!m) return [['Model', '—']];
162
- return [['Spec', m.spec, true], ['Harness', m.harness, true], ['Model', m.model, true], ['Effort', m.effort], m.clamped ? ['Clamped', m.clamped] : ['Clamped', undefined]];
173
+ return [['Spec', m.spec ? plate(m.spec) : undefined], ['Harness', m.harness, true], ['Model', m.model, true], ['Effort', m.effort], m.clamped ? ['Clamped', m.clamped] : ['Clamped', undefined]];
163
174
  }
164
175
 
165
176
  /* ---------- views ---------- */
177
+ // the run's record: what it was asked, how it was set up, and what verification printed (the header names and counts it)
178
+ function runRecord(host, ctx) {
179
+ const run = ctx.run, solo = run.kind === 'solo';
180
+ const s = section(host, 'Settings');
181
+ fields(s, [['Repo', midPath(run.repo), true], ['Run ID', run.run_id, true], ['Path', midPath(run.path), true], ['Source', run.source],
182
+ ['Decompose', run.decompose_level], ['Updated', time(run.updated)], ['Explore', run.settings && run.settings.explore], ['Shadow rate', run.settings && run.settings.shadow_rate],
183
+ ['Shadow reviewers', run.settings && plates(list(run.settings.shadow_qa))], ['QA threshold', run.settings && run.settings.qa_threshold],
184
+ ['Max parallel', run.settings && run.settings.max_parallel], ['Final reviewer', run.settings && run.settings.final_qa ? plate(run.settings.final_qa) : undefined],
185
+ ['Timeout (seconds)', run.settings && run.settings.timeout], ['Verification', chip((run.verification && run.verification.status) || run.verification_status)],
186
+ ['Command', run.verification && run.verification.command, true]]);
187
+ details(host, 'Request', display(run.request), 'run-request');
188
+ if (!solo) details(host, 'Waves', list(run.waves).map((ids, i) => `${i + 1}: ${list(ids).join(', ')}`).join('\n') || '—', 'waves');
189
+ if (run.verification && run.verification.log_tail != null) details(host, 'Verification output', run.verification.log_tail, 'verification-output');
190
+ }
166
191
  function runView(host, ctx) {
167
192
  const run = ctx.run, now = ctx.now;
168
- const head = el('div', undefined, 'insp-head');
169
- head.append(el('div', run.kind === 'solo' ? 'solo run' : 'run', 'insp-kind'), el('h2', String(run.goal || 'Untitled run').split('\n')[0]));
170
- const chips = el('div', undefined, 'chips');
171
- for (const [s, n] of Object.entries(run.counts || {})) { const c = chip(s); c.append(document.createTextNode(' ' + n)); chips.append(c); }
172
- head.append(chips); host.append(head);
173
193
  const tr = RM.timeRange(run, now);
174
194
  const s = section(host, 'Timeline');
175
195
  fields(s, [['Started', time(tr && tr.start)], ['Last activity', time(tr && tr.end)], ['Wall time', tr ? RM.fmtDur(tr.end - tr.start) : '—'],
@@ -196,13 +216,14 @@ window.ForgePanel = (() => {
196
216
  if (F.final.findings) findingCounts(f, F.final.findings);
197
217
  }
198
218
  if (list(run.deferrals).length) details(host, `Deferred tasks (${run.deferrals.length})`, run.deferrals.join('\n'), 'deferrals');
219
+ runRecord(host, ctx);
199
220
  muted(host, 'Select a task, an attempt or a finish node on the map or in the table to see its prompts, models, timings and findings.');
200
221
  }
201
222
 
202
223
  function taskView(host, ctx, task) {
203
224
  const run = ctx.run, now = ctx.now, solo = run.kind === 'solo';
204
225
  const head = el('div', undefined, 'insp-head');
205
- head.append(el('div', solo ? 'solo task' : `task · wave ${display(task.wave)}`, 'insp-kind'), el('h2', solo ? (task.title || 'Solo run') : `${task.id} · ${task.title || 'Untitled task'}`));
226
+ head.append(el('h2', solo ? (task.title || 'Solo run') : `${task.id} · ${task.title || 'Untitled task'}`), el('div', solo ? 'solo task' : `task · wave ${display(task.wave)}`, 'insp-kind'));
206
227
  const chips = el('div', undefined, 'chips');
207
228
  chips.append(chip(task.status));
208
229
  if (task.verdict) chips.append(chip('review ' + task.verdict, task.verdict.toLowerCase()));
@@ -215,7 +236,7 @@ window.ForgePanel = (() => {
215
236
 
216
237
  // the models: who owned it, how it was routed, what it was escalated to
217
238
  const ms = section(host, 'Models');
218
- fields(ms, [['Dwarf', task.dwarf, true], ['Reviewer', task.qa, true], ['Attempt', task.attempt]]);
239
+ fields(ms, [['Dwarf', plate(task.dwarf)], ['Reviewer', plate(task.qa)], ['Attempt', task.attempt]]);
219
240
  const route = list(task.route).slice(-1)[0];
220
241
  if (route) fields(ms, [['Routed', `${display(route.chosen)} from ${list(route.candidates).join(', ') || '—'}${route.explored ? ` (exploratory, p=${display(route.propensity)})` : ''}`, true], ['Tier · ε', `${display(route.tier)} · ${display(route.epsilon)}`]]);
221
242
  for (const e of list(task.escalations)) muted(ms, `Escalated after attempt ${display(e.attempt)}: ${display(e.from)} → ${display(e.to)} at ${time(e.at)}`);
@@ -240,14 +261,18 @@ window.ForgePanel = (() => {
240
261
  const p = tp.parts.find(x => x.id === pid); if (!p || p.planned) continue;
241
262
  const line = el('div', undefined, 'exec-line');
242
263
  const e = p.exec && list(task.executions).find(x => x.id === p.exec.id) || null;
243
- const label = `${p.label.toLowerCase()} · ${p.spec || '—'}`;
264
+ const label = p.label.toLowerCase();
244
265
  const target = p.exec && p.exec.id ? { kind: 'execution', task: task.id, id: task.id, exec: p.exec.id, shadow: p.kind === 'shadow' } : null;
245
- line.append(target ? link(label, ctx.hash(target), () => ctx.onSelect(target)) : el('span', label), chip(p.status));
266
+ line.append(target ? link(label, ctx.hash(target), () => ctx.onSelect(target)) : el('span', label), plate(p.spec), chip(p.status));
246
267
  if (e) line.append(el('span', `${RM.fmtDur(e.duration_s ?? (e.state === 'running' && num(e.started) !== null ? now - e.started : null))}${e.state === 'running' && e.phase ? ' · ' + e.phase : ''}`, 'muted'));
247
- if (p.kind === 'shadow' && p.shadow) line.append(el('span', p.shadow.agreement === 'disagree' ? '⚑ disagrees with the primary review' : p.shadow.agreement === 'agree' ? 'agrees' : '', p.shadow.agreement === 'disagree' ? 'warn' : 'muted'));
268
+ if (p.kind === 'shadow' && p.shadow) line.append(el('span', p.shadow.agreement === 'disagree' ? 'disagrees with the primary review' : p.shadow.agreement === 'agree' ? 'agrees' : '', p.shadow.agreement === 'disagree' ? 'warn' : 'muted'));
248
269
  const fs = (e && e.findings) || (p.shadow && p.shadow.findings);
249
- if (fs && fs.worst) line.append(chip(`${fs.worst}×${fs[fs.worst]}`, SEV[fs.worst]));
250
- if ((p.kind === 'qa' || p.kind === 'shadow') && p.exec && p.exec.id) { const a = diffLink(ctx, 'diff it read', { kind: 'task', task: task.id, source: p.exec.id }, 'diff-link small'); if (a) line.append(a); }
270
+ if (fs && fs.worst) line.append(sevChip(`${fs.worst}×${fs[fs.worst]}`, fs.worst));
271
+ // what it said and ran, and the diff a review read
272
+ const go = el('span', undefined, 'exec-links');
273
+ if (target && ctx.convoHash) { const a = el('a', 'conversation', 'diff-link small'); a.href = ctx.convoHash(target); go.append(a); }
274
+ if ((p.kind === 'qa' || p.kind === 'shadow') && p.exec && p.exec.id) { const a = diffLink(ctx, 'diff it read', { kind: 'task', task: task.id, source: p.exec.id }, 'diff-link small'); if (a) go.append(a); }
275
+ if (go.childElementCount) line.append(go);
251
276
  box.append(line);
252
277
  }
253
278
  for (const l of tp.links.filter(l => l.kind === 'escalation' && z.parts.includes(l.to))) box.append(el('p', `escalated: ${l.label}`, 'muted'));
@@ -268,7 +293,7 @@ window.ForgePanel = (() => {
268
293
  reviews.forEach((r, i) => {
269
294
  const box = el('div', undefined, 'review');
270
295
  const h = el('div', undefined, 'attempt-head');
271
- h.append(el('strong', `${r.kind} review · attempt ${display(r.attempt)}`), el('span', r.spec || '', 'mono'), chip(r.verdict || r.state || 'unknown'));
296
+ h.append(el('strong', `${r.kind} review · attempt ${display(r.attempt)}`), plate(r.spec), chip(r.verdict || r.state || 'unknown'));
272
297
  box.append(h);
273
298
  if (r.state === 'running') muted(box, 'Still reviewing; findings appear when it finishes.');
274
299
  else findingsList(box, r.findings, `task-${task.id}-r${i}`);
@@ -284,7 +309,7 @@ window.ForgePanel = (() => {
284
309
  const cs = section(host, 'Clef predictions');
285
310
  for (const [label, p] of preds) {
286
311
  const n = Number(String(label).match(/\d+/) || 1), outcome = list(task.attempts).filter(a => a.attempt === n).pop();
287
- fields(cs, [[label, `p(pass) = ${display(p.p)} → ${outcome ? outcome.status : 'not finished'}`], ['Assignment', p.assignment ? `${display(p.assignment.dwarf)} / ${display(p.assignment.qa)}` : '—', true]]);
312
+ fields(cs, [[label, `p(pass) = ${display(p.p)} → ${outcome ? outcome.status : 'not finished'}`], ['Assignment', p.assignment ? plates([p.assignment.dwarf, p.assignment.qa]) : '—']]);
288
313
  }
289
314
  }
290
315
  // the prompts and the final messages
@@ -308,12 +333,12 @@ window.ForgePanel = (() => {
308
333
  const e = entry.data || brief;
309
334
  const head = el('div', undefined, 'insp-head');
310
335
  const role = e ? e.role : String(sel.exec).split('-')[0];
311
- head.append(el('div', `${sel.shadow ? 'shadow ' : ''}${role === 'qa' ? 'review' : role} · attempt ${display(e && e.attempt)}`, 'insp-kind'),
312
- el('h2', `${task ? task.id + ' · ' : ''}${role === 'qa' ? 'reviewer' : role === 'dwarf' ? 'dwarf' : role}`));
336
+ head.append(el('h2', `${task ? task.id + ' · ' : ''}${role === 'qa' ? 'reviewer' : role === 'dwarf' ? 'dwarf' : role}`), el('div', `${sel.shadow ? 'shadow ' : ''}${role === 'qa' ? 'review' : role} · attempt ${display(e && e.attempt)}`, 'insp-kind'));
313
337
  const chips = el('div', undefined, 'chips');
314
338
  if (e) { chips.append(chip(RM.execStatus(e))); if (e.verdict) chips.append(chip('verdict ' + e.verdict, e.verdict.toLowerCase())); }
315
339
  head.append(chips);
316
340
  if (task) head.append(link('← task ' + task.id, ctx.hash({ kind: 'task', id: task.id }), () => ctx.onSelect({ kind: 'task', id: task.id })));
341
+ if ((role === 'qa' || role === 'dwarf') && ctx.convoHash) { const a = el('a', 'Its conversation: what it said and ran →', 'diff-link'); a.href = ctx.convoHash(sel); head.append(a); }
317
342
  if (task && (role === 'qa' || role === 'dwarf')) { const a = diffLink(ctx, role === 'qa' ? 'The diff this review read →' : 'The diff its review read →', { kind: 'task', task: task.id, source: sel.exec }); if (a) head.append(a); }
318
343
  host.append(head);
319
344
  if (!e) { loading(host, entry, 'this execution'); return; }
@@ -367,7 +392,8 @@ window.ForgePanel = (() => {
367
392
  li.append(el('i', undefined, 'led st-' + String(n.ui_status || 'unknown').toLowerCase()));
368
393
  const target = { kind: 'fractal', task: task.id, node: n.id };
369
394
  li.append(link(n.id, ctx.hash(target), () => ctx.onSelect(target)));
370
- const bits = [n.reserved ? 'reserved' : n.status, n.difficulty, n.model, n.phase ? `${n.phase}${n.iteration ? ' #' + n.iteration : ''}` : null, num(n.cost) !== null ? money(n.cost) : null].filter(Boolean);
395
+ const bits = [n.reserved ? 'reserved' : n.status, n.difficulty, n.phase ? `${n.phase}${n.iteration ? ' #' + n.iteration : ''}` : null, num(n.cost) !== null ? money(n.cost) : null].filter(Boolean);
396
+ if (n.model) li.append(plate(n.model));
371
397
  li.append(el('span', bits.join(' · '), 'muted'));
372
398
  const dec = n.decomposition;
373
399
  if (dec && dec.decision) li.append(el('span', dec.decision === 'split' ? `split into ${list(dec.children).length}` : dec.decision, 'tag'));
@@ -383,7 +409,7 @@ window.ForgePanel = (() => {
383
409
  const run = ctx.run, path = `${fractalPath(run, sel.task)}/nodes/${encodeURIComponent(sel.node)}`;
384
410
  const entry = lazy(ctx, path, run.updated), n = entry.data;
385
411
  const head = el('div', undefined, 'insp-head');
386
- head.append(el('div', `Fractal node · task ${sel.task}`, 'insp-kind'), el('h2', sel.node));
412
+ head.append(el('h2', sel.node), el('div', `Fractal node · task ${sel.task}`, 'insp-kind'));
387
413
  const back = { kind: 'task', id: sel.task };
388
414
  if (n) { const chips = el('div', undefined, 'chips'); chips.append(chip(n.reserved ? 'reserved' : n.ui_status || n.status, n.reserved ? 'none' : undefined)); if (n.decomposition && n.decomposition.decision) chips.append(el('span', n.decomposition.decision, 'tag')); head.append(chips); }
389
415
  head.append(link('← task ' + sel.task, ctx.hash(back), () => ctx.onSelect(back)));
@@ -392,7 +418,7 @@ window.ForgePanel = (() => {
392
418
  if (loading(host, entry, 'this node')) return;
393
419
  if (n.goal) host.append(el('p', n.goal));
394
420
  fields(host, [['Parent', n.parent ? link(n.parent, ctx.hash({ kind: 'fractal', task: sel.task, node: n.parent }), () => ctx.onSelect({ kind: 'fractal', task: sel.task, node: n.parent })) : '—'],
395
- ['Difficulty', n.difficulty], ['Model', n.model, true], ['Resolved', n.resolved_model, true], ['Paths', list(n.paths).join('\n') || '—', true], ['Depends on', list(n.deps).join(', ') || '—', true],
421
+ ['Difficulty', n.difficulty], ['Model', n.model ? plate(n.model) : '—'], ['Resolved', n.resolved_model ? plate(n.resolved_model) : '—'], ['Paths', list(n.paths).join('\n') || '—', true], ['Depends on', list(n.deps).join(', ') || '—', true],
396
422
  ['Phase', n.phase ? `${n.phase}${n.iteration ? ' · iteration ' + n.iteration : ''}` : '—'], ['Started', time(n.started_at)], ['Ended', time(n.ended_at)],
397
423
  ['Cost', `${money(n.cost)}${n.unknown_cost_steps ? ` (+${n.unknown_cost_steps} steps of unknown cost)` : ''}`], ['Tokens', n.tokens ? `${display(n.tokens.input_tokens)} in (${display(n.tokens.cached_input_tokens)} cached) · ${display(n.tokens.output_tokens)} out` : '—']]);
398
424
  if (n.error) host.append(el('p', n.error, 'warn'));
@@ -404,7 +430,7 @@ window.ForgePanel = (() => {
404
430
  const adm = list(dec.admission);
405
431
  if (adm.length) {
406
432
  const ul = el('ul', undefined, 'plain');
407
- for (const a of adm) { const li = el('li'); li.append(link(a.name, ctx.hash({ kind: 'fractal', task: sel.task, node: a.name }), () => ctx.onSelect({ kind: 'fractal', task: sel.task, node: a.name })), document.createTextNode(` · ${a.difficulty || '—'} · ${a.model || '—'} — ${a.goal || ''}`)); ul.append(li); }
433
+ for (const a of adm) { const li = el('li'); li.append(link(a.name, ctx.hash({ kind: 'fractal', task: sel.task, node: a.name }), () => ctx.onSelect({ kind: 'fractal', task: sel.task, node: a.name })), document.createTextNode(` · ${a.difficulty || '—'} · `), plate(a.model), document.createTextNode(` — ${a.goal || ''}`)); ul.append(li); }
408
434
  ds.append(el('p', 'Children it admitted:', 'muted'), ul);
409
435
  }
410
436
  if (dec.error) ds.append(el('p', dec.error, 'warn'));
@@ -419,7 +445,7 @@ window.ForgePanel = (() => {
419
445
  function finalView(host, ctx, sel) {
420
446
  const run = ctx.run, reviews = list(run.final_reviews);
421
447
  const head = el('div', undefined, 'insp-head');
422
- head.append(el('div', 'finish · whole-run review', 'insp-kind'), el('h2', 'Final review'));
448
+ head.append(el('h2', 'Final review'), el('div', 'finish · whole-run review', 'insp-kind'));
423
449
  host.append(head);
424
450
  const pick = sel.name ? reviews.find(r => r.name === sel.name) : reviews[0];
425
451
  if (!pick) {
@@ -433,7 +459,7 @@ window.ForgePanel = (() => {
433
459
  if (pick.published) chips.append(el('span', 'published', 'tag'));
434
460
  chips.append(el('span', pick.source === 'integrate' ? 'during integrate' : 'forge review', 'tag'));
435
461
  head.append(chips);
436
- fields(host, [['Reviewer', pick.spec, true], ['Threshold', pick.threshold], ['Reviewed', pick.base || pick.tip ? `${String(pick.base || '?').slice(0, 10)}..${String(pick.tip || '?').slice(0, 10)}` : '—', true],
462
+ fields(host, [['Reviewer', plate(pick.spec)], ['Threshold', pick.threshold], ['Reviewed', pick.base || pick.tip ? `${String(pick.base || '?').slice(0, 10)}..${String(pick.tip || '?').slice(0, 10)}` : '—', true],
437
463
  ['Started', time(pick.started ?? pick.at)], ['Ended', time(pick.ended)], ['Duration', RM.fmtDur(pick.duration_s)]]);
438
464
  if (pick.execution) { const s = { kind: 'execution', task: null, exec: pick.execution }; host.append(link('Model, timings and exact prompt →', ctx.hash(s), () => ctx.onSelect(s))); }
439
465
  { const a = diffLink(ctx, 'The combined diff it read, findings pinned →', { kind: 'final', name: pick.name }); if (a) { host.append(el('br')); host.append(a); } }
@@ -457,7 +483,7 @@ window.ForgePanel = (() => {
457
483
  for (const r of reviews) {
458
484
  const li = el('li');
459
485
  li.append(link(`${time(r.started ?? r.at)} · ${r.verdict || r.state}`, ctx.hash({ kind: 'final', name: r.name }), () => ctx.onSelect({ kind: 'final', name: r.name })));
460
- if (r.findings && r.findings.worst) li.append(document.createTextNode(' '), chip(`${r.findings.worst}×${r.findings[r.findings.worst]}`, SEV[r.findings.worst]));
486
+ if (r.findings && r.findings.worst) li.append(document.createTextNode(' '), sevChip(`${r.findings.worst}×${r.findings[r.findings.worst]}`, r.findings.worst));
461
487
  if (r.name === pick.name) li.className = 'current';
462
488
  ul.append(li);
463
489
  }
@@ -468,7 +494,7 @@ window.ForgePanel = (() => {
468
494
  function verificationView(host, ctx) {
469
495
  const run = ctx.run, v = run.verification;
470
496
  const head = el('div', undefined, 'insp-head');
471
- head.append(el('div', 'finish', 'insp-kind'), el('h2', 'Verification'), chip(v && v.status || 'pending'));
497
+ head.append(el('h2', 'Verification'), el('div', 'finish', 'insp-kind'), chip(v && v.status || 'pending'));
472
498
  host.append(head);
473
499
  if (!v) { muted(host, 'Verification has not run: it runs after every task is merged into the integration branch.'); return; }
474
500
  fields(host, [['Command', v.command, true], ['Commit', v.commit, true]]);
@@ -484,7 +510,7 @@ window.ForgePanel = (() => {
484
510
  const run = ctx.run, ig = run.integration || {};
485
511
  const F = RM.finishState(run);
486
512
  const head = el('div', undefined, 'insp-head');
487
- head.append(el('div', 'finish', 'insp-kind'), el('h2', 'Integration'), chip(F.integration.status));
513
+ head.append(el('h2', 'Integration'), el('div', 'finish', 'insp-kind'), chip(F.integration.status));
488
514
  host.append(head);
489
515
  fields(host, [['Merged', `${F.integration.merged} of ${F.integration.total} tasks`], ['Base', ig.base, true], ['Accepted at', ig.accepted, true]]);
490
516
  if (list(ig.merged).length) {
@@ -509,12 +535,22 @@ window.ForgePanel = (() => {
509
535
  host.replaceChildren();
510
536
  const run = ctx.run, sel = ctx.sel;
511
537
  if (!run) return;
512
- const close = sel && !(run.kind === 'solo' && sel.kind === 'task') ? link('Close ✕', ctx.hash(null), () => ctx.onSelect(null)) : null;
513
- if (close) { close.className = 'insp-close'; host.append(close); }
538
+ const close = sel && !(run.kind === 'solo' && sel.kind === 'task') ? link('', ctx.hash(null), () => ctx.onSelect(null)) : null;
539
+ if (close) {
540
+ close.className = 'insp-close button'; close.textContent = '';
541
+ if (window.ForgeIcons) close.append(window.ForgeIcons.icon('close'));
542
+ close.append(document.createTextNode('Close')); close.title = 'Back to the run (Esc on the stage)';
543
+ host.append(close);
544
+ }
514
545
  const tid = sel && (sel.kind === 'task' ? sel.id : sel.kind === 'execution' ? sel.task : null);
515
546
  const task = tid ? list(run.tasks).find(t => t.id === tid) : run.kind === 'solo' && sel && sel.kind === 'execution' ? list(run.tasks)[0] : null;
516
547
  if (!sel) return runView(host, ctx);
517
- if (sel.kind === 'task') { if (!task) { muted(host, `Task ${sel.id} is not in this run.`); return; } return taskView(host, ctx, task); }
548
+ if (sel.kind === 'task') {
549
+ if (!task) { muted(host, `Task ${sel.id} is not in this run.`); return; }
550
+ taskView(host, ctx, task);
551
+ if (run.kind === 'solo') runRecord(host, ctx); // a solo run is its one task: its record goes below it
552
+ return;
553
+ }
518
554
  if (sel.kind === 'execution') return execView(host, ctx, task, sel);
519
555
  if (sel.kind === 'final') return finalView(host, ctx, sel);
520
556
  if (sel.kind === 'verification') return verificationView(host, ctx);
@@ -508,6 +508,22 @@
508
508
  if (!s.length) return null;
509
509
  return { start: Math.min(...s), end: Math.max(...e, ...s) };
510
510
  }
511
+ // A task's route through the plan: the tasks it waits on, however indirectly (ancestors), and the tasks that wait
512
+ // on it (descendants). Unknown ids and dependency cycles end the walk instead of looping.
513
+ function lineage(run, id) {
514
+ const up = new Map(), down = new Map();
515
+ for (const t of list(run && run.tasks)) {
516
+ if (!t || t.id == null) continue;
517
+ up.set(t.id, list(t.deps));
518
+ for (const d of list(t.deps)) { if (!down.has(d)) down.set(d, []); down.get(d).push(t.id); }
519
+ }
520
+ const walk = next => {
521
+ const out = new Set(), stack = [...(next.get(id) || [])];
522
+ while (stack.length) { const x = stack.pop(); if (x === id || out.has(x) || !up.has(x)) continue; out.add(x); stack.push(...(next.get(x) || [])); }
523
+ return out;
524
+ };
525
+ return { ancestors: walk(up), descendants: walk(down) };
526
+ }
511
527
  // Events worth flashing during a replay: attempts ending, escalations, merges, reviews.
512
528
  function events(run) {
513
529
  const out = [];
@@ -646,6 +662,6 @@
646
662
  }
647
663
 
648
664
  return { liveNow, runState, execStatus, matchRun, filterOptions, atlasModel, columns, planModel, finishState, taskParts,
649
- taskTimes, metricValue, overlap, projectSchedule, execModel, criticalPath, concurrency, stateAt, timeRange, events, structureKey, innerKey,
665
+ taskTimes, metricValue, overlap, projectSchedule, execModel, criticalPath, concurrency, stateAt, timeRange, events, lineage, structureKey, innerKey,
650
666
  fmtDur, alias, basename, weight, searchIndex, searchRank, statusEvents, fractalTree };
651
667
  });