@giamme/forge 0.8.0 → 0.10.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 (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +106 -1766
  3. package/SKILL.md +22 -3
  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 +84 -0
  8. package/docs/commands.md +365 -0
  9. package/docs/dataset.md +29 -0
  10. package/docs/decomposed-runs.md +310 -0
  11. package/docs/development.md +95 -0
  12. package/docs/forge-ui.md +128 -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 +245 -0
  18. package/docs/safety.md +55 -0
  19. package/docs/troubleshooting.md +54 -0
  20. package/forge +20 -0
  21. package/package.json +3 -2
  22. package/references/dataset.md +18 -1
  23. package/references/decompose.md +20 -2
  24. package/references/fractal.md +1 -1
  25. package/references/harnesses.md +18 -6
  26. package/references/solo.md +1 -1
  27. package/references/ui.md +27 -5
  28. package/scripts/forge-config.py +347 -0
  29. package/scripts/forge-control.py +645 -0
  30. package/scripts/forge-dispatch.sh +7 -1
  31. package/scripts/forge-issues.py +806 -0
  32. package/scripts/forge-lib-task.sh +234 -35
  33. package/scripts/forge-parallel-options.sh +3 -1
  34. package/scripts/forge-parallel.sh +3 -2
  35. package/scripts/forge-route.py +184 -27
  36. package/scripts/forge-runs-rm.py +840 -0
  37. package/scripts/forge-runs.py +139 -0
  38. package/scripts/forge-runtime.py +59 -6
  39. package/scripts/forge-schedule.py +227 -11
  40. package/scripts/forge-ui.py +85 -19
  41. package/scripts/forge-version.py +54 -12
  42. package/scripts/forge_dataset/__init__.py +77 -6
  43. package/scripts/forge_dataset/capture.py +5 -2
  44. package/scripts/forge_dataset/harvest.py +23 -1
  45. package/scripts/forge_dataset/importer.py +420 -18
  46. package/scripts/forge_fractal/__init__.py +2 -1
  47. package/scripts/forge_registry.py +168 -16
  48. package/ui/README.md +116 -23
  49. package/ui/THIRD_PARTY_NOTICES.md +108 -0
  50. package/ui/docs/api.md +283 -23
  51. package/ui/forge_ui/__main__.py +10 -2
  52. package/ui/forge_ui/control.py +575 -0
  53. package/ui/forge_ui/export.py +33 -9
  54. package/ui/forge_ui/fractal.py +1 -2
  55. package/ui/forge_ui/reader.py +121 -8
  56. package/ui/forge_ui/server.py +135 -5
  57. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  58. package/ui/forge_ui/static/app.js +358 -131
  59. package/ui/forge_ui/static/cloud3d.js +3 -2
  60. package/ui/forge_ui/static/cloudui.js +8 -5
  61. package/ui/forge_ui/static/controls.js +619 -0
  62. package/ui/forge_ui/static/convo.js +295 -0
  63. package/ui/forge_ui/static/datatable.js +232 -0
  64. package/ui/forge_ui/static/diffview.js +29 -10
  65. package/ui/forge_ui/static/findings3d.js +134 -0
  66. package/ui/forge_ui/static/findingsview.js +412 -0
  67. package/ui/forge_ui/static/graph.js +3 -3
  68. package/ui/forge_ui/static/icons.js +94 -0
  69. package/ui/forge_ui/static/index.html +31 -13
  70. package/ui/forge_ui/static/iso.js +70 -19
  71. package/ui/forge_ui/static/leaderboard.js +44 -48
  72. package/ui/forge_ui/static/panel.js +92 -36
  73. package/ui/forge_ui/static/rundelete.js +388 -0
  74. package/ui/forge_ui/static/runmodel.js +17 -1
  75. package/ui/forge_ui/static/scene3d.js +518 -43
  76. package/ui/forge_ui/static/search.js +2 -1
  77. package/ui/forge_ui/static/settings.js +399 -0
  78. package/ui/forge_ui/static/shell.js +137 -0
  79. package/ui/forge_ui/static/stagectl.js +150 -0
  80. package/ui/forge_ui/static/style.css +1099 -469
  81. package/ui/forge_ui/static/theme.js +25 -0
  82. package/ui/forge_ui/static/uikit.js +161 -0
  83. package/ui/forge_ui/transcripts.py +608 -0
@@ -1,6 +1,7 @@
1
1
  /* forge-ui inspector: what a selected node is, which model owned it, what prompt launched it, how long it
2
2
  took and what its reviewer found. Defines window.ForgePanel.render(host, ctx). All API values enter the
3
- DOM as text. Large texts (prompts, reports) come from the lazy endpoints and are fetched once per node. */
3
+ DOM as text. Large texts (prompts, reports) come from the lazy endpoints and are fetched once per node.
4
+ The run's and a task's write controls are controls.js's (ForgeControls); this file only says where they stand. */
4
5
  window.ForgePanel = (() => {
5
6
  'use strict';
6
7
  const RM = window.ForgeRunModel;
@@ -9,7 +10,6 @@ window.ForgePanel = (() => {
9
10
  const display = v => v === null || v === undefined || v === '' ? '—' : typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v);
10
11
  const STATUSES = new Set(['merged', 'pass', 'running', 'pending', 'fail', 'error', 'infra', 'timeout', 'unknown', 'invalidated', 'blocked',
11
12
  'interrupted', 'nochanges', 'conflict', 'accepted', 'none']);
12
- const SEV = { P0: 'fail', P1: 'conflict', P2: 'timeout', P3: 'unknown' };
13
13
  const cache = new Map(); // path → {data, error, at}
14
14
  function el(tag, text, className) {
15
15
  const node = document.createElement(tag);
@@ -21,6 +21,18 @@ window.ForgePanel = (() => {
21
21
  const s = String(value || 'unknown').toLowerCase();
22
22
  return el('span', value || '—', 'chip st-' + (cls || (STATUSES.has(s) ? s : 'unknown')));
23
23
  }
24
+ // a finding's severity: its own shape and ramp (style.css .sev-P0…P3), never a status lamp
25
+ const plate = spec => window.ForgeIcons ? window.ForgeIcons.modelPlate(spec) : el('span', spec || '—', 'mono');
26
+ const plates = specs => { const s = el('span', undefined, 'plates'); for (const x of specs) s.append(plate(x)); return specs.length ? s : undefined; };
27
+ // a long path cut in the middle: its head gives way, its last part always shows; copying takes all of it
28
+ function midPath(v) {
29
+ const s = String(v || ''), i = s.lastIndexOf('/', s.length - 2);
30
+ if (!s) return undefined;
31
+ const out = el('span', undefined, 'mid-path'); out.title = s;
32
+ out.append(el('span', i > 0 ? s.slice(0, i + 1) : '', 'mid-head'), el('span', i > 0 ? s.slice(i + 1) : s, 'mid-tail'));
33
+ return out;
34
+ }
35
+ const sevChip = (text, sev) => el('span', text, 'chip sev-chip sev-' + (/^P[0-3]$/.test(sev) ? sev : 'none'));
24
36
  function time(v) {
25
37
  if (num(v) === null) return '—';
26
38
  const d = new Date(v * 1000); return Number.isNaN(d.getTime()) ? '—' : d.toLocaleString();
@@ -48,6 +60,15 @@ window.ForgePanel = (() => {
48
60
  return a;
49
61
  }
50
62
  function muted(parent, text) { parent.append(el('p', text, 'muted')); }
63
+ // who decided an escalation: policy, a human's retry --dwarf (retry) or a pin; older rows read as policy
64
+ const sourcePlate = source => el('span', source || 'policy', 'plate src-plate src-' + String(source || 'policy').replace(/[^a-z]/g, ''));
65
+ // the write controls of a plan run (task undefined) or of one of its tasks; a page without controls.js says it is read-only
66
+ function controlsSection(host, ctx, task) {
67
+ if (ctx.run.kind === 'solo') return;
68
+ if (window.ForgeControls && window.ForgeUI && window.ForgeUI.confirmInline) { window.ForgeControls[task ? 'mountTask' : 'mountRun'](host, ctx, task); return; }
69
+ const s = section(host, 'Controls');
70
+ muted(s, 'Read-only: this page cannot change the run.');
71
+ }
51
72
  // a link into the diff viewer (ctx.diffHash is absent where there is no viewer)
52
73
  function diffLink(ctx, text, spec, cls = 'diff-link') { if (!ctx.diffHash) return null; const a = el('a', text, cls); a.href = ctx.diffHash(spec); return a; }
53
74
  // a lazy resource: returns cached data, or starts the fetch and re-renders when it lands
@@ -87,7 +108,7 @@ window.ForgePanel = (() => {
87
108
  const row = el('div', undefined, 'chips');
88
109
  if (!f) { row.append(el('span', 'no findings parsed', 'muted')); parent.append(row); return; }
89
110
  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; }
111
+ for (const sev of ['P0', 'P1', 'P2', 'P3']) if (f[sev]) { row.append(sevChip(`${sev} × ${f[sev]}`, sev)); any = true; }
91
112
  if (f.unlabelled) { row.append(chip(`unlabelled × ${f.unlabelled}`, 'unknown')); any = true; }
92
113
  if (f.plausible) { row.append(chip(`plausible × ${f.plausible}`, 'pending')); any = true; }
93
114
  if (!any) row.append(el('span', 'no confirmed findings', 'muted'));
@@ -124,7 +145,7 @@ window.ForgePanel = (() => {
124
145
  findings.slice().sort((a, b) => order(a) - order(b)).forEach((f, i) => {
125
146
  const li = el('li', undefined, 'finding' + (f.status === 'CONFIRMED' ? '' : ' plausible'));
126
147
  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'));
148
+ head.append(sevChip(f.severity || 'P?', f.severity), el('span', f.status, 'tag'));
128
149
  if (f.location) head.append(el('code', f.location));
129
150
  li.append(head, el('p', f.summary || ''));
130
151
  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 +180,26 @@ window.ForgePanel = (() => {
159
180
  const tokens = t => t ? `${display(t.input)} in (${display(t.cached)} cached) · ${display(t.output)} out` : '—';
160
181
  function modelFields(m) {
161
182
  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]];
183
+ 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
184
  }
164
185
 
165
186
  /* ---------- views ---------- */
187
+ // the run's record: what it was asked, how it was set up, and what verification printed (the header names and counts it)
188
+ function runRecord(host, ctx) {
189
+ const run = ctx.run, solo = run.kind === 'solo';
190
+ const s = section(host, 'Settings');
191
+ fields(s, [['Repo', midPath(run.repo), true], ['Run ID', run.run_id, true], ['Path', midPath(run.path), true], ['Source', run.source],
192
+ ['Decompose', run.decompose_level], ['Updated', time(run.updated)], ['Explore', run.settings && run.settings.explore], ['Shadow rate', run.settings && run.settings.shadow_rate],
193
+ ['Shadow reviewers', run.settings && plates(list(run.settings.shadow_qa))], ['QA threshold', run.settings && run.settings.qa_threshold],
194
+ ['Max parallel', run.settings && run.settings.max_parallel], ['Final reviewer', run.settings && run.settings.final_qa ? plate(run.settings.final_qa) : undefined],
195
+ ['Timeout (seconds)', run.settings && run.settings.timeout], ['Verification', chip((run.verification && run.verification.status) || run.verification_status)],
196
+ ['Command', run.verification && run.verification.command, true]]);
197
+ details(host, 'Request', display(run.request), 'run-request');
198
+ if (!solo) details(host, 'Waves', list(run.waves).map((ids, i) => `${i + 1}: ${list(ids).join(', ')}`).join('\n') || '—', 'waves');
199
+ if (run.verification && run.verification.log_tail != null) details(host, 'Verification output', run.verification.log_tail, 'verification-output');
200
+ }
166
201
  function runView(host, ctx) {
167
202
  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
203
  const tr = RM.timeRange(run, now);
174
204
  const s = section(host, 'Timeline');
175
205
  fields(s, [['Started', time(tr && tr.start)], ['Last activity', time(tr && tr.end)], ['Wall time', tr ? RM.fmtDur(tr.end - tr.start) : '—'],
@@ -196,13 +226,15 @@ window.ForgePanel = (() => {
196
226
  if (F.final.findings) findingCounts(f, F.final.findings);
197
227
  }
198
228
  if (list(run.deferrals).length) details(host, `Deferred tasks (${run.deferrals.length})`, run.deferrals.join('\n'), 'deferrals');
229
+ controlsSection(host, ctx);
230
+ runRecord(host, ctx);
199
231
  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
232
  }
201
233
 
202
234
  function taskView(host, ctx, task) {
203
235
  const run = ctx.run, now = ctx.now, solo = run.kind === 'solo';
204
236
  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'}`));
237
+ 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
238
  const chips = el('div', undefined, 'chips');
207
239
  chips.append(chip(task.status));
208
240
  if (task.verdict) chips.append(chip('review ' + task.verdict, task.verdict.toLowerCase()));
@@ -215,10 +247,14 @@ window.ForgePanel = (() => {
215
247
 
216
248
  // the models: who owned it, how it was routed, what it was escalated to
217
249
  const ms = section(host, 'Models');
218
- fields(ms, [['Dwarf', task.dwarf, true], ['Reviewer', task.qa, true], ['Attempt', task.attempt]]);
250
+ fields(ms, [['Dwarf', plate(task.dwarf)], ['Reviewer', plate(task.qa)], ['Attempt', task.attempt]]);
219
251
  const route = list(task.route).slice(-1)[0];
220
252
  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
- 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)}`);
253
+ for (const e of list(task.escalations)) {
254
+ const line = el('p', `Escalated after attempt ${display(e.attempt)}: ${display(e.from)} → ${display(e.to)} at ${time(e.at)} `, 'muted');
255
+ line.append(sourcePlate(e.source)); ms.append(line);
256
+ }
257
+ controlsSection(host, ctx, task);
222
258
 
223
259
  // timings
224
260
  const tt = RM.taskTimes(task, run, now);
@@ -229,7 +265,7 @@ window.ForgePanel = (() => {
229
265
 
230
266
  // attempts, each with its dwarf and its review
231
267
  const as = section(host, 'Attempts');
232
- const tp = RM.taskParts(task);
268
+ const tp = RM.taskParts(task), usedEscalations = new Set();
233
269
  if (!list(task.executions).length) muted(as, 'No executions recorded yet' + (task.status === 'PENDING' ? ': this task has not started.' : '.'));
234
270
  for (const z of tp.zones) {
235
271
  const box = el('div', undefined, 'attempt');
@@ -240,17 +276,26 @@ window.ForgePanel = (() => {
240
276
  const p = tp.parts.find(x => x.id === pid); if (!p || p.planned) continue;
241
277
  const line = el('div', undefined, 'exec-line');
242
278
  const e = p.exec && list(task.executions).find(x => x.id === p.exec.id) || null;
243
- const label = `${p.label.toLowerCase()} · ${p.spec || '—'}`;
279
+ const label = p.label.toLowerCase();
244
280
  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));
281
+ line.append(target ? link(label, ctx.hash(target), () => ctx.onSelect(target)) : el('span', label), plate(p.spec), chip(p.status));
246
282
  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'));
283
+ 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
284
  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); }
285
+ if (fs && fs.worst) line.append(sevChip(`${fs.worst}×${fs[fs.worst]}`, fs.worst));
286
+ // what it said and ran, and the diff a review read
287
+ const go = el('span', undefined, 'exec-links');
288
+ if (target && ctx.convoHash) { const a = el('a', 'conversation', 'diff-link small'); a.href = ctx.convoHash(target); go.append(a); }
289
+ 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); }
290
+ if (go.childElementCount) line.append(go);
291
+ box.append(line);
292
+ }
293
+ for (const l of tp.links.filter(l => l.kind === 'escalation' && z.parts.includes(l.to))) {
294
+ const line = el('p', `escalated: ${l.label} `, 'muted');
295
+ const e = list(task.escalations).find(x => !usedEscalations.has(x) && `${x.from || '?'} → ${x.to || '?'}` === l.label); // the link carries only its label
296
+ if (e) { usedEscalations.add(e); line.append(sourcePlate(e.source)); }
251
297
  box.append(line);
252
298
  }
253
- for (const l of tp.links.filter(l => l.kind === 'escalation' && z.parts.includes(l.to))) box.append(el('p', `escalated: ${l.label}`, 'muted'));
254
299
  as.append(box);
255
300
  }
256
301
 
@@ -268,7 +313,7 @@ window.ForgePanel = (() => {
268
313
  reviews.forEach((r, i) => {
269
314
  const box = el('div', undefined, 'review');
270
315
  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'));
316
+ h.append(el('strong', `${r.kind} review · attempt ${display(r.attempt)}`), plate(r.spec), chip(r.verdict || r.state || 'unknown'));
272
317
  box.append(h);
273
318
  if (r.state === 'running') muted(box, 'Still reviewing; findings appear when it finishes.');
274
319
  else findingsList(box, r.findings, `task-${task.id}-r${i}`);
@@ -284,7 +329,7 @@ window.ForgePanel = (() => {
284
329
  const cs = section(host, 'Clef predictions');
285
330
  for (const [label, p] of preds) {
286
331
  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]]);
332
+ fields(cs, [[label, `p(pass) = ${display(p.p)} → ${outcome ? outcome.status : 'not finished'}`], ['Assignment', p.assignment ? plates([p.assignment.dwarf, p.assignment.qa]) : '—']]);
288
333
  }
289
334
  }
290
335
  // the prompts and the final messages
@@ -308,12 +353,12 @@ window.ForgePanel = (() => {
308
353
  const e = entry.data || brief;
309
354
  const head = el('div', undefined, 'insp-head');
310
355
  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}`));
356
+ 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
357
  const chips = el('div', undefined, 'chips');
314
358
  if (e) { chips.append(chip(RM.execStatus(e))); if (e.verdict) chips.append(chip('verdict ' + e.verdict, e.verdict.toLowerCase())); }
315
359
  head.append(chips);
316
360
  if (task) head.append(link('← task ' + task.id, ctx.hash({ kind: 'task', id: task.id }), () => ctx.onSelect({ kind: 'task', id: task.id })));
361
+ 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
362
  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
363
  host.append(head);
319
364
  if (!e) { loading(host, entry, 'this execution'); return; }
@@ -367,7 +412,8 @@ window.ForgePanel = (() => {
367
412
  li.append(el('i', undefined, 'led st-' + String(n.ui_status || 'unknown').toLowerCase()));
368
413
  const target = { kind: 'fractal', task: task.id, node: n.id };
369
414
  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);
415
+ 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);
416
+ if (n.model) li.append(plate(n.model));
371
417
  li.append(el('span', bits.join(' · '), 'muted'));
372
418
  const dec = n.decomposition;
373
419
  if (dec && dec.decision) li.append(el('span', dec.decision === 'split' ? `split into ${list(dec.children).length}` : dec.decision, 'tag'));
@@ -383,7 +429,7 @@ window.ForgePanel = (() => {
383
429
  const run = ctx.run, path = `${fractalPath(run, sel.task)}/nodes/${encodeURIComponent(sel.node)}`;
384
430
  const entry = lazy(ctx, path, run.updated), n = entry.data;
385
431
  const head = el('div', undefined, 'insp-head');
386
- head.append(el('div', `Fractal node · task ${sel.task}`, 'insp-kind'), el('h2', sel.node));
432
+ head.append(el('h2', sel.node), el('div', `Fractal node · task ${sel.task}`, 'insp-kind'));
387
433
  const back = { kind: 'task', id: sel.task };
388
434
  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
435
  head.append(link('← task ' + sel.task, ctx.hash(back), () => ctx.onSelect(back)));
@@ -392,7 +438,7 @@ window.ForgePanel = (() => {
392
438
  if (loading(host, entry, 'this node')) return;
393
439
  if (n.goal) host.append(el('p', n.goal));
394
440
  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],
441
+ ['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
442
  ['Phase', n.phase ? `${n.phase}${n.iteration ? ' · iteration ' + n.iteration : ''}` : '—'], ['Started', time(n.started_at)], ['Ended', time(n.ended_at)],
397
443
  ['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
444
  if (n.error) host.append(el('p', n.error, 'warn'));
@@ -404,7 +450,7 @@ window.ForgePanel = (() => {
404
450
  const adm = list(dec.admission);
405
451
  if (adm.length) {
406
452
  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); }
453
+ 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
454
  ds.append(el('p', 'Children it admitted:', 'muted'), ul);
409
455
  }
410
456
  if (dec.error) ds.append(el('p', dec.error, 'warn'));
@@ -419,7 +465,7 @@ window.ForgePanel = (() => {
419
465
  function finalView(host, ctx, sel) {
420
466
  const run = ctx.run, reviews = list(run.final_reviews);
421
467
  const head = el('div', undefined, 'insp-head');
422
- head.append(el('div', 'finish · whole-run review', 'insp-kind'), el('h2', 'Final review'));
468
+ head.append(el('h2', 'Final review'), el('div', 'finish · whole-run review', 'insp-kind'));
423
469
  host.append(head);
424
470
  const pick = sel.name ? reviews.find(r => r.name === sel.name) : reviews[0];
425
471
  if (!pick) {
@@ -433,7 +479,7 @@ window.ForgePanel = (() => {
433
479
  if (pick.published) chips.append(el('span', 'published', 'tag'));
434
480
  chips.append(el('span', pick.source === 'integrate' ? 'during integrate' : 'forge review', 'tag'));
435
481
  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],
482
+ 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
483
  ['Started', time(pick.started ?? pick.at)], ['Ended', time(pick.ended)], ['Duration', RM.fmtDur(pick.duration_s)]]);
438
484
  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
485
  { 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 +503,7 @@ window.ForgePanel = (() => {
457
503
  for (const r of reviews) {
458
504
  const li = el('li');
459
505
  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]));
506
+ if (r.findings && r.findings.worst) li.append(document.createTextNode(' '), sevChip(`${r.findings.worst}×${r.findings[r.findings.worst]}`, r.findings.worst));
461
507
  if (r.name === pick.name) li.className = 'current';
462
508
  ul.append(li);
463
509
  }
@@ -468,7 +514,7 @@ window.ForgePanel = (() => {
468
514
  function verificationView(host, ctx) {
469
515
  const run = ctx.run, v = run.verification;
470
516
  const head = el('div', undefined, 'insp-head');
471
- head.append(el('div', 'finish', 'insp-kind'), el('h2', 'Verification'), chip(v && v.status || 'pending'));
517
+ head.append(el('h2', 'Verification'), el('div', 'finish', 'insp-kind'), chip(v && v.status || 'pending'));
472
518
  host.append(head);
473
519
  if (!v) { muted(host, 'Verification has not run: it runs after every task is merged into the integration branch.'); return; }
474
520
  fields(host, [['Command', v.command, true], ['Commit', v.commit, true]]);
@@ -484,7 +530,7 @@ window.ForgePanel = (() => {
484
530
  const run = ctx.run, ig = run.integration || {};
485
531
  const F = RM.finishState(run);
486
532
  const head = el('div', undefined, 'insp-head');
487
- head.append(el('div', 'finish', 'insp-kind'), el('h2', 'Integration'), chip(F.integration.status));
533
+ head.append(el('h2', 'Integration'), el('div', 'finish', 'insp-kind'), chip(F.integration.status));
488
534
  host.append(head);
489
535
  fields(host, [['Merged', `${F.integration.merged} of ${F.integration.total} tasks`], ['Base', ig.base, true], ['Accepted at', ig.accepted, true]]);
490
536
  if (list(ig.merged).length) {
@@ -509,12 +555,22 @@ window.ForgePanel = (() => {
509
555
  host.replaceChildren();
510
556
  const run = ctx.run, sel = ctx.sel;
511
557
  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); }
558
+ const close = sel && !(run.kind === 'solo' && sel.kind === 'task') ? link('', ctx.hash(null), () => ctx.onSelect(null)) : null;
559
+ if (close) {
560
+ close.className = 'insp-close button'; close.textContent = '';
561
+ if (window.ForgeIcons) close.append(window.ForgeIcons.icon('close'));
562
+ close.append(document.createTextNode('Close')); close.title = 'Back to the run (Esc on the stage)';
563
+ host.append(close);
564
+ }
514
565
  const tid = sel && (sel.kind === 'task' ? sel.id : sel.kind === 'execution' ? sel.task : null);
515
566
  const task = tid ? list(run.tasks).find(t => t.id === tid) : run.kind === 'solo' && sel && sel.kind === 'execution' ? list(run.tasks)[0] : null;
516
567
  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); }
568
+ if (sel.kind === 'task') {
569
+ if (!task) { muted(host, `Task ${sel.id} is not in this run.`); return; }
570
+ taskView(host, ctx, task);
571
+ if (run.kind === 'solo') runRecord(host, ctx); // a solo run is its one task: its record goes below it
572
+ return;
573
+ }
518
574
  if (sel.kind === 'execution') return execView(host, ctx, task, sel);
519
575
  if (sel.kind === 'final') return finalView(host, ctx, sel);
520
576
  if (sel.kind === 'verification') return verificationView(host, ctx);