@zenera/cli 1.1.25 → 1.1.27

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 (96) hide show
  1. package/README.md +13 -1
  2. package/dist/catalog.d.ts +20 -0
  3. package/dist/catalog.js +60 -0
  4. package/dist/commands/index.js +2 -0
  5. package/dist/commands/inspect.d.ts +5 -1
  6. package/dist/commands/inspect.js +54 -14
  7. package/dist/commands/memory.js +28 -3
  8. package/dist/commands/meta.js +24 -5
  9. package/dist/commands/models.js +2 -1
  10. package/dist/commands/sandbox.js +73 -3
  11. package/dist/commands/view.d.ts +8 -0
  12. package/dist/commands/view.js +114 -0
  13. package/dist/engine.d.ts +3 -1
  14. package/dist/engine.js +6 -0
  15. package/dist/eventlog.d.ts +5 -0
  16. package/dist/eventlog.js +25 -1
  17. package/dist/liveness.js +20 -2
  18. package/dist/main.js +0 -0
  19. package/dist/meta/editor.d.ts +1 -1
  20. package/dist/meta/editor.js +4 -21
  21. package/dist/meta/finetune/analyze-brief.d.ts +26 -0
  22. package/dist/meta/finetune/analyze-brief.js +131 -0
  23. package/dist/meta/finetune/apply-brief.d.ts +23 -0
  24. package/dist/meta/finetune/apply-brief.js +247 -0
  25. package/dist/meta/finetune/brief.d.ts +45 -0
  26. package/dist/meta/finetune/brief.js +158 -0
  27. package/dist/meta/finetune/command.d.ts +1 -1
  28. package/dist/meta/finetune/command.js +224 -46
  29. package/dist/meta/finetune/cost.d.ts +46 -0
  30. package/dist/meta/finetune/cost.js +105 -0
  31. package/dist/meta/finetune/dataset/types.d.ts +3 -1
  32. package/dist/meta/finetune/feedback.d.ts +14 -2
  33. package/dist/meta/finetune/feedback.js +22 -3
  34. package/dist/meta/finetune/guard.d.ts +29 -0
  35. package/dist/meta/finetune/guard.js +104 -0
  36. package/dist/meta/finetune/improvements.d.ts +50 -3
  37. package/dist/meta/finetune/improvements.js +290 -33
  38. package/dist/meta/finetune/live.d.ts +17 -7
  39. package/dist/meta/finetune/live.js +56 -10
  40. package/dist/meta/finetune/loop.js +59 -10
  41. package/dist/meta/finetune/recheck.d.ts +58 -0
  42. package/dist/meta/finetune/recheck.js +311 -0
  43. package/dist/meta/finetune/report.d.ts +9 -2
  44. package/dist/meta/finetune/report.js +551 -438
  45. package/dist/meta/finetune/sampler.js +8 -2
  46. package/dist/meta/finetune/train.d.ts +6 -2
  47. package/dist/meta/finetune/train.js +55 -21
  48. package/dist/meta/finetune/tui.d.ts +3 -3
  49. package/dist/meta/finetune/tui.js +37 -12
  50. package/dist/meta/finetune/tuning.d.ts +27 -1
  51. package/dist/meta/finetune/tuning.js +21 -2
  52. package/dist/meta/finetune/usage.d.ts +4 -0
  53. package/dist/meta/finetune/usage.js +35 -0
  54. package/dist/meta/finetune/view.d.ts +9 -0
  55. package/dist/meta/finetune/view.js +27 -0
  56. package/dist/meta/host.d.ts +2 -1
  57. package/dist/meta/host.js +2 -1
  58. package/dist/meta.d.ts +291 -0
  59. package/dist/meta.js +981 -0
  60. package/dist/resources.d.ts +215 -0
  61. package/dist/resources.js +856 -0
  62. package/dist/scaffold.d.ts +3 -4
  63. package/dist/scaffold.js +21 -9
  64. package/dist/trace.d.ts +5 -0
  65. package/dist/trace.js +54 -5
  66. package/dist/tui/app.js +19 -5
  67. package/dist/tui/links.d.ts +27 -0
  68. package/dist/tui/links.js +142 -0
  69. package/dist/tui/models.d.ts +3 -1
  70. package/dist/tui/models.js +65 -7
  71. package/dist/tui/sandbox.d.ts +10 -0
  72. package/dist/tui/sandbox.js +131 -0
  73. package/package.json +3 -2
  74. package/templates/editor/.github/copilot-instructions.md +29 -43
  75. package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
  76. package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
  77. package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
  78. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
  79. package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
  80. package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
  81. package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
  82. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
  83. package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
  84. package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
  85. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
  86. package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
  87. package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
  88. package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
  89. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
  90. package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
  91. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
  92. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
  93. package/templates/project/agents/fork-instructions.md +78 -28
  94. package/templates/project/agents/memory-instructions.md +222 -493
  95. package/templates/project/agents/tools-instructions.md +7 -0
  96. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
@@ -1,13 +1,13 @@
1
- import { MERMAID_URL } from '@zenera/neo';
2
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
3
2
  import { basename, dirname, join, relative, sep } from 'node:path';
4
3
  import { safe } from "../host.js";
5
4
  import { DatasetStore } from "./dataset/store.js";
6
5
  import { DATASET_DIR } from "./dataset/types.js";
7
- import { feedbackId, readFeedback } from "./feedback.js";
6
+ import { feedbackId, forDataset, lead, readFeedback, } from "./feedback.js";
7
+ import { RECHECK_TRIES } from "./recheck.js";
8
8
  import { resultOf, selection } from "./sampler.js";
9
9
  import { readJson } from "./tuning.js";
10
- import { envelopesIn, mergeModels, mergeStages, runMetrics, runTokens, short, STAGES, total, } from "./usage.js";
10
+ import { applyTokens, envelopesIn, mergeModels, mergeStages, runMetrics, runTokens, short, STAGES, total, } from "./usage.js";
11
11
  const PHASE_WORD = { nomem: 'no memory', mem: 'with memory' };
12
12
  function atomic(file, text) {
13
13
  mkdirSync(dirname(file), { recursive: true });
@@ -19,6 +19,12 @@ export const cell = (text) => String(text ?? '')
19
19
  .replace(/\s*\n\s*/g, ' ')
20
20
  .replace(/\|/g, '\\|')
21
21
  .trim() || '-';
22
+ /** Markdown as a blockquote, paragraphs kept. */
23
+ const quote = (text) => text
24
+ .trim()
25
+ .split('\n')
26
+ .map((l) => (l.trim() ? `> ${l}` : '>'))
27
+ .join('\n');
22
28
  /** A link from `from` (a directory) to `to`, safe for spaces. */
23
29
  const link = (label, from, to) => `[${label}](<${relative(from, to).split(sep).join('/')}>)`;
24
30
  export function since(ms) {
@@ -32,6 +38,15 @@ export function since(ms) {
32
38
  }
33
39
  return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`;
34
40
  }
41
+ /** The analyze session of one try, kept in its folder. */
42
+ export function analyzeSession(dir) {
43
+ try {
44
+ return readFileSync(join(dir, 'session'), 'utf8').trim() || undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
35
50
  export function attemptsOf(t, c) {
36
51
  const dir = t.caseDir(c);
37
52
  let names = [];
@@ -57,11 +72,7 @@ export function attemptsOf(t, c) {
57
72
  .sort((a, b) => Number(a.phase === 'mem') - Number(b.phase === 'mem') || a.attempt - b.attempt);
58
73
  }
59
74
  function decisions(t) {
60
- const all = {};
61
- for (const a of t.improvements.applies()) {
62
- Object.assign(all, readJson(join(a.dir, 'decisions.json')) ?? {});
63
- }
64
- return all;
75
+ return t.improvements.decisions();
65
76
  }
66
77
  /** A run's tokens per model: recorded at the run, or read from its trajectory. */
67
78
  function runByModel(run) {
@@ -115,9 +126,14 @@ function tokenTable(by) {
115
126
  '',
116
127
  ];
117
128
  }
118
- const rubricScore = (f) => {
129
+ export const rubricScore = (f) => {
119
130
  const r = Object.values(f?.rubric ?? {});
120
- return r.length ? `${r.filter((x) => x === 'pass').length}/${r.length}` : '-';
131
+ const invalid = r.filter((x) => x === 'invalid').length;
132
+ const graded = r.length - invalid;
133
+ if (!r.length) {
134
+ return '-';
135
+ }
136
+ return `${r.filter((x) => x === 'pass').length}/${graded}${invalid ? ` (${invalid} invalid)` : ''}`;
121
137
  };
122
138
  // ---------------------------------------------------------------------------
123
139
  // What a run did, and how it moved from the try it is compared with
@@ -375,9 +391,10 @@ export function writeFeedback(t, c) {
375
391
  const attempts = attemptsOf(t, c);
376
392
  const latest = [...attempts].reverse().find((a) => a.feedback);
377
393
  const decided = decisions(t);
378
- const session = existsSync(join(t.caseDir(c), 'session'))
379
- ? readFileSync(join(t.caseDir(c), 'session'), 'utf8').trim()
380
- : undefined;
394
+ const session = [...attempts]
395
+ .reverse()
396
+ .map((a) => analyzeSession(a.dir))
397
+ .find(Boolean);
381
398
  const lines = [];
382
399
  const say = (...more) => {
383
400
  lines.push(...more);
@@ -400,7 +417,10 @@ export function writeFeedback(t, c) {
400
417
  const f = latest.feedback;
401
418
  say(`## Latest analysis - ${PHASE_WORD[latest.phase]}, try ${latest.attempt}: ${f.verdict ?? '?'}, rubric ${rubricScore(f)}${f.done ? ', done' : ''}`, '');
402
419
  if (f.summary) {
403
- say(`> ${cell(f.summary)}`, '');
420
+ say(quote(f.summary), '');
421
+ }
422
+ if (latest.run?.missedSkills?.length) {
423
+ say(`Never loaded the skill(s) an apply made for it: ${latest.run.missedSkills.map((s) => `\`${s}\``).join(', ')}.`, '');
404
424
  }
405
425
  const id = feedbackId(c.id, latest.phase, latest.attempt, t.round(c));
406
426
  const improvements = f.improvements ?? [];
@@ -408,11 +428,13 @@ export function writeFeedback(t, c) {
408
428
  say('### Improvements', '', '| # | Change | File | Decision |', '| --- | --- | --- | --- |');
409
429
  for (const i of improvements) {
410
430
  const d = decided[id]?.[i.id];
411
- const decision = d?.decision
412
- ? `${d.decision}${d.note ? `: ${d.note}` : ''}`
413
- : t.improvements.isApplied(id)
414
- ? 'applied (no decision recorded)'
415
- : 'waiting for an apply';
431
+ const decision = forDataset(i)
432
+ ? 'for the dataset - `zen meta dataset`, never an apply'
433
+ : d?.decision
434
+ ? `${d.decision}${d.note ? `: ${d.note}` : ''}`
435
+ : t.improvements.isApplied(id)
436
+ ? 'applied (no decision recorded)'
437
+ : 'waiting for an apply';
416
438
  say(`| ${cell(i.id)} | ${cell(i.change)} | ${cell(i.file)} | ${cell(decision)} |`);
417
439
  }
418
440
  say('');
@@ -476,13 +498,12 @@ export function writeFeedback(t, c) {
476
498
  }
477
499
  const md = `${lines.join('\n')}\n`;
478
500
  atomic(file, md);
479
- atomic(join(here, 'FEEDBACK.html'), markdownPage(`${c.id} - ${basename(t.root)}`, md, !t.finished && !resultOf(t, c)));
480
501
  }
481
502
  // ---------------------------------------------------------------------------
482
503
  // APPLY.md - one apply: every request, every decision, every file it changed
483
504
  // ---------------------------------------------------------------------------
484
505
  /** A fence longer than any run of backticks in `text`, so nothing inside can close it. */
485
- function fence(text) {
506
+ export function fence(text) {
486
507
  const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((r) => r.length));
487
508
  return '`'.repeat(Math.max(3, longest + 1));
488
509
  }
@@ -526,12 +547,15 @@ export function writeApply(t, dir) {
526
547
  : '';
527
548
  const files = patchFiles(patch);
528
549
  const improvements = inputs.reduce((n, f) => n + f.improvements.length, 0);
550
+ const consolidation = applied?.kind === 'consolidate' || existsSync(join(dir, 'consolidate.json'));
529
551
  const status = failed
530
552
  ? 'failed - the project was put back as it was'
531
553
  : applied
532
- ? applied.skipped
533
- ? 'nothing to change'
534
- : 'applied'
554
+ ? applied.discarded
555
+ ? `undone - ${applied.discarded}`
556
+ : applied.skipped
557
+ ? 'nothing to change'
558
+ : 'applied'
535
559
  : 'in progress';
536
560
  const started = applied?.startedAt ??
537
561
  failed?.startedAt ??
@@ -549,22 +573,32 @@ export function writeApply(t, dir) {
549
573
  }
550
574
  }
551
575
  const out = [
552
- `# Apply ${name}${applied ? ` - v${applied.from} → v${applied.to}` : ''}`,
576
+ `# ${consolidation ? 'Consolidation' : 'Apply'} ${name}${applied && !applied.discarded ? ` - v${applied.from} → v${applied.to}` : ''}`,
553
577
  '',
554
578
  link('← status', here, join(t.dir, 'STATUS.md')),
555
579
  '',
556
580
  '| Status | Started | Finished | Took | Requests | Improvements | Files changed | Retried |',
557
581
  '| --- | --- | --- | --- | --- | --- | --- | --- |',
558
- `| ${status} | ${when(started)} | ${when(finished)} | ${took} | ${inputs.length} | ${improvements} | ${files.length} | ${retried ? 'yes - the first attempt failed zen check' : 'no'} |`,
582
+ `| ${cell(status)} | ${when(started)} | ${when(finished)} | ${took} | ${inputs.length} | ${improvements} | ${files.length} | ${retried ? 'yes - the first attempt was undone' : 'no'} |`,
559
583
  '',
560
584
  ];
585
+ if (consolidation) {
586
+ out.push('A consolidation answers no request: it may only shrink what the agents read, and change no rule.', '');
587
+ }
561
588
  if (failed) {
562
589
  out.push(`> ${cell(failed.error)}`, '');
563
590
  }
564
591
  if (envelope?.sessionId) {
565
592
  out.push(`The apply session: \`zen meta resume ${basename(t.root)} ${envelope.sessionId}\` - ask it why it changed what it changed.`, '');
566
593
  }
567
- out.push('## Decisions', '', '| Applied | Merged | Rejected | Structural | Undecided |', '| --- | --- | --- | --- | --- |', `| ${tally.applied ?? 0} | ${tally.merged ?? 0} | ${tally.rejected ?? 0} | ${tally.structural ?? 0} | ${tally.undecided ?? 0} |`, '', '## Requests', '');
594
+ if (!consolidation) {
595
+ const deferred = readJson(join(dir, 'deferred.json')) ?? [];
596
+ out.push('## Decisions', '', '| Applied | Merged | Rejected | Deferred | Structural | Undecided |', '| --- | --- | --- | --- | --- | --- |', `| ${tally.applied ?? 0} | ${tally.merged ?? 0} | ${tally.rejected ?? 0} | ${tally.deferred ?? 0} | ${tally.structural ?? 0} | ${tally.undecided ?? 0} |`, '');
597
+ if (deferred.length > 0) {
598
+ out.push(`${deferred.length} improvement(s) held back by earlier applies were offered again: ${link('deferred.json', here, join(dir, 'deferred.json'))}.`, '');
599
+ }
600
+ out.push('## Requests', '');
601
+ }
568
602
  inputs.forEach((f, n) => {
569
603
  const tryDir = f.file
570
604
  ? dirname(f.file)
@@ -581,9 +615,9 @@ export function writeApply(t, dir) {
581
615
  run ? link('run', here, run.dir) : '',
582
616
  run?.report ? link('report', here, run.report) : '',
583
617
  ].filter(Boolean);
584
- out.push(`### ${n + 1}. ${f.case} - ${PHASE_WORD[f.phase]}, try ${f.attempt}: ${f.verdict}, rubric ${rubricScore(f)}`, '', `\`${f.id}\` · ${links.join(' · ')}`, '');
618
+ out.push(`### ${n + 1}. ${f.case} - ${f.recheck ? `recheck after ${f.recheck}` : PHASE_WORD[f.phase]}, try ${f.attempt}: ${f.verdict}, rubric ${rubricScore(f)}`, '', `\`${f.id}\` · ${links.join(' · ')}`, '');
585
619
  if (f.summary) {
586
- out.push(`> ${cell(f.summary)}`, '');
620
+ out.push(quote(f.summary), '');
587
621
  }
588
622
  if (f.improvements.length === 0) {
589
623
  out.push('No improvements asked for.', '');
@@ -622,24 +656,78 @@ export function writeApply(t, dir) {
622
656
  const check = join(dir, 'check.log');
623
657
  if (existsSync(check)) {
624
658
  const tail = readFileSync(check, 'utf8').trimEnd().split('\n').slice(-40).join('\n');
625
- const verdict = failed
626
- ? 'failed on both attempts'
627
- : retried
628
- ? 'failed once; the edit was undone and the second attempt passed'
629
- : applied
630
- ? 'passed'
631
- : 'running';
659
+ const verdict = failed ? 'failed on both attempts' : applied ? 'passed' : 'running';
632
660
  const tick = fence(tail);
633
661
  out.push(verdict, '', `${tick}text`, tail || '(no output)', tick, '');
634
662
  }
635
663
  else {
636
664
  out.push(applied?.skipped ? '_Not run: nothing was changed._' : '_Not run yet._', '');
637
665
  }
638
- if (applied) {
639
- out.push('## Cases sent back to run', '', ...inputs.map((f) => `- ${link(f.case, here, feedbackPath(t, { id: f.case }))} - ${PHASE_WORD[f.phase]}, try ${f.attempt + 1} on v${applied.to}`), '');
666
+ out.push(...gates(t, dir, applied));
667
+ if (applied && !consolidation) {
668
+ out.push('## Cases sent back to run', '', ...inputs.map((f) => f.recheck
669
+ ? `- ${link(f.case, here, feedbackPath(t, { id: f.case }))} - regressed after ${f.recheck}; rechecked first on v${applied.to}`
670
+ : `- ${link(f.case, here, feedbackPath(t, { id: f.case }))} - ${PHASE_WORD[f.phase]}, try ${f.attempt + 1} on v${applied.to}`), '');
640
671
  }
672
+ out.push(...recheckSection(t, here, (r) => r.apply === name));
641
673
  atomic(applyPath(dir), `${out.join('\n')}\n`);
642
674
  }
675
+ /** The budget: what the edit cost every later call. */
676
+ function gates(t, dir, applied) {
677
+ const out = [];
678
+ const budget = readJson(join(dir, 'budget.json'));
679
+ if (budget) {
680
+ out.push('## The prose', '', 'What every call reads, in bytes, summed over the agents: house rules, prompt, preloaded skills, skill index lines.', '', '| At the start | Before | After | Budget | Ceiling |', '| --- | --- | --- | --- | --- |', `| ${budget.start.total} | ${budget.total} | ${applied?.bytes?.after ?? '-'} | +${budget.budget} | ${budget.ceiling} |`, '');
681
+ const over = join(dir, 'budget.md');
682
+ if (existsSync(over)) {
683
+ out.push(readFileSync(over, 'utf8').trim().replace(/^#/gm, '###'), '');
684
+ }
685
+ const moved = join(dir, 'cost.md');
686
+ if (existsSync(moved)) {
687
+ out.push(readFileSync(moved, 'utf8').trim().replace(/^#/gm, '###'), '');
688
+ }
689
+ }
690
+ return out;
691
+ }
692
+ const RECHECK_WORD = {
693
+ queued: 'queued',
694
+ running: 'running',
695
+ passed: 'passed',
696
+ regressed: 'regressed',
697
+ void: 'void',
698
+ };
699
+ /** Rechecks, newest first: on an apply's page its own, on STATUS.md the latest. */
700
+ function recheckSection(t, here, which, limit = 50) {
701
+ const all = t.rechecks.all().filter(which);
702
+ if (all.length === 0) {
703
+ return [];
704
+ }
705
+ const n = (s) => all.filter((r) => r.state === s).length;
706
+ const out = [
707
+ '## Rechecks',
708
+ '',
709
+ `Cases that passed before, run again without memory on the version an apply made. One that fails both of its ${RECHECK_TRIES} tries has regressed, and its fixes join the improvement queue.`,
710
+ '',
711
+ `passed **${n('passed')}** · regressed **${n('regressed')}** · void **${n('void')}** · queued or running **${n('queued') + n('running')}**`,
712
+ '',
713
+ '| Apply | Case | State | Tries | Request | Note |',
714
+ '| --- | --- | --- | --- | --- | --- |',
715
+ ];
716
+ for (const r of [...all].reverse().slice(0, limit)) {
717
+ const tries = t.rechecks
718
+ .tryDirs(r)
719
+ .map((d, i) => {
720
+ const word = r.tries?.[i] ?? 'running';
721
+ return existsSync(join(d, 'analysis.md'))
722
+ ? link(word, here, join(d, 'analysis.md'))
723
+ : link(word, here, d);
724
+ })
725
+ .join(', ');
726
+ out.push(`| ${link(r.apply, here, applyPath(join(t.improvements.dir, r.apply)))} | ${link(r.case, here, feedbackPath(t, { id: r.case }))} | ${RECHECK_WORD[r.state]} | ${tries || '-'} | ${cell(r.request)} | ${cell(r.note)} |`);
727
+ }
728
+ out.push('');
729
+ return out;
730
+ }
643
731
  // ---------------------------------------------------------------------------
644
732
  // The memory section of STATUS.md
645
733
  // ---------------------------------------------------------------------------
@@ -677,256 +765,315 @@ function memorySection(t, here) {
677
765
  // ---------------------------------------------------------------------------
678
766
  // STATUS.md - the whole tuning
679
767
  // ---------------------------------------------------------------------------
680
- const STEP_CLASS = {
681
- idle: 'idle',
682
- run: 'run',
683
- analyze: 'analyze',
684
- parked: 'parked',
685
- };
686
- // One diagram per worker: the path its case takes through both phases, with
687
- // the step it is on now filled in.
768
+ /** Longest live line in a sentence, and in a diagram box. */
769
+ const NOW_CELL = 100;
770
+ const NOW_LABEL = 60;
771
+ const cut = (text, max) => text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
772
+ /** One line, every mark that would style it escaped. */
773
+ const escaped = (text) => text
774
+ .replace(/\s+/g, ' ')
775
+ .trim()
776
+ .replace(/[\\`*_[\]<>|~&]/g, '\\$&');
777
+ /** A live line in running text: cut, and escaped. */
778
+ export const liveCell = (text) => text ? escaped(cut(text.replace(/\s+/g, ' ').trim(), NOW_CELL)) : '-';
779
+ /**
780
+ * Several live lines in one cell, oldest first. Each ends in `<br>`, so zen view draws
781
+ * one row per line, cut at the width the column gets rather than wrapped.
782
+ */
783
+ const liveCells = (lines) => lines?.length ? lines.map((l) => `${escaped(l)}<br>`).join('') : '-';
784
+ /** A live line in a diagram label: only what `safe` lets through, so it cannot break out. */
785
+ const liveLabel = (text) => text ? cut(safe(text, Number.MAX_SAFE_INTEGER), NOW_LABEL) : '';
786
+ /** What a worker's agent is doing, if the live snapshot has it on the step the worker is on. */
787
+ function nowOf(t, w) {
788
+ return liveRow(t, w)?.now;
789
+ }
790
+ const liveRow = (t, w) => t.live?.now.find((r) => r.key === `w${w.slot}` && r.step === w.step);
791
+ const applierRow = (t) => t.live?.now.find((r) => r.key === 'apply');
792
+ /** What a step under way has spent so far; it reaches the files only when the step ends. */
793
+ const inFlight = (row) => row?.stage && row.act ? { [row.stage]: row.act.byModel } : {};
794
+ const applierNow = (t) => applierRow(t)?.now;
688
795
  function idleWord(t) {
689
796
  if (t.improvements.applying) {
690
797
  return 'idle - no run starts during an apply';
691
798
  }
692
799
  return t.drained ? 'idle - no case left to start' : 'idle';
693
800
  }
694
- function workerDiagram(t, w) {
695
- if (!w.case) {
696
- return [
697
- '```mermaid',
698
- 'flowchart LR',
699
- ` w["worker ${w.slot} · ${idleWord(t)} · ${since(Date.now() - w.since)}"]`,
700
- ' classDef idle fill:#6e7781,color:#fff',
701
- ' class w idle',
702
- '```',
703
- ];
704
- }
705
- const phase = w.phase ?? 'nomem';
706
- const p = phase === 'mem' ? 'm' : 'n';
707
- const current = `${p}${w.step === 'analyze' ? 'a' : w.step === 'parked' ? 'p' : 'r'}`;
801
+ const DIAGRAM_HEAD = [
802
+ '```mermaid',
803
+ '---',
804
+ 'config:',
805
+ // Only the base theme takes theme variables.
806
+ ' theme: base',
807
+ ' flowchart:',
808
+ ' wrappingWidth: 480',
809
+ ' themeVariables:',
810
+ " lineColor: '#8c959f'",
811
+ " edgeLabelBackground: '#ffffff'",
812
+ '---',
813
+ ];
814
+ // One palette for every diagram: solid fills for what is moving now, tints for the rest.
815
+ const PALETTE = [
816
+ ' classDef case fill:#ffffff,stroke:#57606a,color:#24292f',
817
+ ' classDef idle fill:#f6f8fa,stroke:#d0d7de,color:#8c959f',
818
+ ' classDef run fill:#0969da,stroke:#0550ae,color:#ffffff',
819
+ ' classDef analyze fill:#8250df,stroke:#6639ba,color:#ffffff',
820
+ ' classDef off fill:#ffffff,stroke:#d0d7de,color:#57606a',
821
+ ' classDef busy fill:#e16f24,stroke:#bc4c00,color:#ffffff',
822
+ ' classDef full fill:#fff8c5,stroke:#d4a72c,color:#4d2d00',
823
+ ' classDef empty fill:#ffffff,stroke:#d0d7de,stroke-dasharray:4 3,color:#8c959f',
824
+ ' classDef kept fill:#d8f3f1,stroke:#1b7c83,color:#0b3d40',
825
+ ' classDef done fill:#dafbe1,stroke:#1a7f37,color:#116329',
826
+ ' classDef bad fill:#ffebe9,stroke:#cf222e,color:#a40e26',
827
+ ' classDef warn fill:#fff1e5,stroke:#e16f24,color:#953800',
828
+ ' classDef store fill:#ddf4ff,stroke:#54aeff,color:#0a3069',
829
+ ' classDef hub fill:#eaeef2,stroke:#8c959f,color:#24292f',
830
+ ];
831
+ /** `click` lines for a diagram: only to an anchor or to a file that exists, relative to the page. */
832
+ function clickable(t) {
833
+ const clicks = [];
834
+ return {
835
+ clicks,
836
+ click: (id, to, tip) => {
837
+ if (to && (to.startsWith('#') || existsSync(to))) {
838
+ const url = to.startsWith('#')
839
+ ? to
840
+ : encodeURI(relative(t.dir, to).split(sep).join('/'));
841
+ clicks.push(` click ${id} href "${url}" "${tip}"`);
842
+ }
843
+ },
844
+ };
845
+ }
846
+ // Where every case is now: each step of its path, with how many cases are on it.
847
+ function pathDiagram(t, counts) {
708
848
  const q = t.improvements;
709
- const tries = phase === 'mem' ? t.config.memTries : t.config.tries;
710
- const extra = (id) => {
711
- if (id !== current) {
712
- return '';
713
- }
714
- const now = ` · try ${w.attempt} of ${tries} · ${since(Date.now() - w.since)}`;
715
- return w.step === 'parked'
716
- ? `${now} · queue ${q.pending.length} of ${t.config.applyAt}${q.applying ? ' · applying' : ''}`
717
- : now;
849
+ const at = (phase) => {
850
+ const mine = t.workers.filter((w) => w.case && (w.phase ?? 'nomem') === phase);
851
+ return {
852
+ run: mine.filter((w) => w.step !== 'analyze').length,
853
+ grade: mine.filter((w) => w.step === 'analyze').length,
854
+ wait: [...q.pending, ...q.inApply].filter((f) => f.phase === phase).length,
855
+ };
718
856
  };
719
- const node = (id, text) => ` ${id}["${text}${extra(id)}"]`;
720
- const out = [
721
- '```mermaid',
722
- 'flowchart LR',
723
- node('nr', 'no memory run'),
724
- node('na', 'no memory analyze'),
725
- node('np', 'no memory parked'),
726
- node('mr', 'with memory run'),
727
- node('ma', 'with memory analyze'),
728
- node('mp', 'with memory parked'),
729
- ' cd["completed"]',
730
- ' df["difficult"]',
731
- ' nr --> na',
732
- ' na -->|"passed"| mr',
733
- ' na -->|"failed, tries left"| np',
734
- ' np -->|"applied"| nr',
735
- ' na -->|"no tries left"| df',
736
- ' mr --> ma',
737
- ' ma -->|"cheaper"| cd',
738
- ' ma -->|"failed, tries left"| mp',
739
- ' mp -->|"applied"| mr',
740
- ' ma -->|"no tries left"| df',
741
- ' classDef run fill:#0969da,color:#fff',
742
- ' classDef analyze fill:#8250df,color:#fff',
743
- ' classDef parked fill:#9a6700,color:#fff',
744
- ' classDef past fill:#d0d7de,color:#24292f',
745
- ' classDef done fill:#1a7f37,color:#fff',
746
- ' classDef bad fill:#cf222e,color:#fff',
747
- ' class cd done',
748
- ' class df bad',
749
- ];
750
- if (phase === 'mem') {
751
- out.push(' class nr,na,np past');
857
+ const nomem = at('nomem');
858
+ const mem = at('mem');
859
+ const paint = {};
860
+ const step = (id, text, n, cls) => {
861
+ (paint[n > 0 ? cls : 'off'] ??= []).push(id);
862
+ return `${id}("${text} · ${n}")`;
863
+ };
864
+ const { clicks, click } = clickable(t);
865
+ const queue = anchor(`Improvement queue - ${q.pending.length} of ${t.config.applyAt} parked`);
866
+ click('NS', join(t.root, DATASET_DIR), 'the dataset');
867
+ click('NW', queue, 'the cases waiting for a fix');
868
+ click('MW', queue, 'the cases waiting for a fix');
869
+ click('CD', '#cases', 'every case');
870
+ if (counts.difficult > 0) {
871
+ click('DF', '#difficult-cases', 'the difficult cases');
872
+ }
873
+ if (counts.failed > 0) {
874
+ click('FL', '#failed-cases', 'the failed cases');
752
875
  }
753
- out.push(` class ${current} ${STEP_CLASS[w.step]}`, '```');
754
- return out;
755
- }
756
- function overviewDiagram(counts) {
757
876
  return [
758
- '```mermaid',
759
- 'flowchart LR',
760
- ` Q["queued · ${counts.queued}"] --> N["no memory · ${counts.nomem}"]`,
761
- ` N --> M["with memory · ${counts.mem}"]`,
762
- ` M --> C["completed · ${counts.completed}"]`,
763
- ` N --> D["difficult · ${counts.difficult}"]`,
764
- ' M --> D',
765
- ` Q -.-> S["stopped · ${counts.stopped}"]`,
766
- ...(counts.failed ? [` N -.-> F["failed · ${counts.failed}"]`, ' M -.-> F'] : []),
767
- ' classDef done fill:#1a7f37,color:#fff',
768
- ' classDef bad fill:#cf222e,color:#fff',
769
- ' classDef live fill:#0969da,color:#fff',
770
- ' classDef broke fill:#9a6700,color:#fff',
771
- ' class C done',
772
- ' class D bad',
773
- ' class N,M live',
774
- ...(counts.failed ? [' class F broke'] : []),
877
+ 'Where every case is right now. A case goes through two rounds, first **without memory**, then **with memory**. In each round it is run, then graded; a case that failed waits for a fix to the instructions and runs again.',
878
+ '',
879
+ ...DIAGRAM_HEAD,
880
+ // Edges end on the round boxes, never inside them, so each round keeps its own row.
881
+ 'flowchart TB',
882
+ ` NS[("not started · ${counts.queued}")]`,
883
+ ' subgraph NOMEM["1 · without memory"]',
884
+ ' direction LR',
885
+ ` ${step('NR', 'running', nomem.run, 'run')} --> ${step('NA', 'grading', nomem.grade, 'analyze')}`,
886
+ ` NA -->|"failed, tries left"| ${step('NW', 'waiting for a fix', nomem.wait, 'full')}`,
887
+ ' NW -->|"fixed"| NR',
888
+ ' end',
889
+ ' subgraph MEM["2 · with memory"]',
890
+ ' direction LR',
891
+ ` ${step('MR', 'running', mem.run, 'run')} --> ${step('MA', 'grading', mem.grade, 'analyze')}`,
892
+ ` MA -->|"failed, tries left"| ${step('MW', 'waiting for a fix', mem.wait, 'full')}`,
893
+ ' MW -->|"fixed"| MR',
894
+ ' end',
895
+ ' NS --> NOMEM',
896
+ ' NOMEM -->|"passed"| MEM',
897
+ ` MEM -->|"passed"| CD("completed · ${counts.completed}")`,
898
+ ` NOMEM -->|"out of tries"| DF("difficult · ${counts.difficult}")`,
899
+ ' MEM -->|"out of tries"| DF',
900
+ ...(counts.stopped > 0
901
+ ? [` ST("stopped part-way · ${counts.stopped}") -.->|"next start"| NOMEM`]
902
+ : []),
903
+ ...(counts.failed > 0
904
+ ? [
905
+ ` NOMEM -.->|"sandbox or provider broke"| FL("${lines(`failed · ${counts.failed}`, 'resumes at the next start')}")`,
906
+ ]
907
+ : []),
908
+ ...clicks,
909
+ ...PALETTE,
910
+ ...Object.entries(paint).map(([cls, ids]) => ` class ${ids.join(',')} ${cls}`),
911
+ ' class NS store',
912
+ ' class CD done',
913
+ ' class DF bad',
914
+ ...(counts.stopped > 0 ? [' class ST idle'] : []),
915
+ ...(counts.failed > 0 ? [' class FL warn'] : []),
916
+ ' style NOMEM fill:#ffffff,stroke:#d0d7de',
917
+ ' style MEM fill:#ffffff,stroke:#d0d7de',
775
918
  '```',
776
919
  ];
777
920
  }
778
- const LANE = {
779
- idle: 'fill:#f6f8fa,stroke:#d0d7de',
780
- parked: 'fill:#f6f8fa,stroke:#d0d7de',
781
- run: 'fill:#ddf4ff,stroke:#0969da,stroke-width:2px',
782
- analyze: 'fill:#fbefff,stroke:#8250df,stroke-width:2px',
921
+ const STEP_WORD = {
922
+ idle: 'idle',
923
+ run: 'running',
924
+ analyze: 'grading',
925
+ parked: 'parked',
783
926
  };
784
- // A subgraph linked from outside drops its own direction; invisible links keep the slots in order.
927
+ // Edges end on the box, so it keeps LR; the invisible chain keeps the slots in order.
785
928
  const inOrder = (prefix, n) => n > 1
786
929
  ? [` ${Array.from({ length: n }, (_, i) => `${prefix}${i + 1}`).join(' ~~~ ')}`]
787
930
  : [];
788
931
  // A markdown label: real newlines, since some previews swallow `<br/>` before Mermaid sees it.
789
- const lines = (...parts) => `\`${parts.join('\n')}\``;
790
- // The whole machine right now: every worker, the queue filling toward an apply, the merge.
791
- // Each node links to what it is working on.
792
- function systemDiagram(t, counts) {
932
+ const lines = (...parts) => `\`${parts.filter(Boolean).join('\n')}\``;
933
+ // The page's own heading ids, so a box can link to the section listing it in full.
934
+ const anchor = (heading) => `#${heading
935
+ .trim()
936
+ .toLowerCase()
937
+ .replace(/[^\w\s-]/g, '')
938
+ .replace(/\s/g, '-')}`;
939
+ const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
940
+ // The whole machine right now: plain words first, then one box per moving part.
941
+ // Each box links to what it is working on, or to the section that lists it.
942
+ function systemDiagram(t, counts, last) {
793
943
  const q = t.improvements;
794
944
  const m = t.memories;
795
- const lit = {
796
- case: [],
797
- run: [],
798
- analyze: [],
799
- off: [],
800
- full: [],
801
- empty: [],
802
- busy: [],
803
- kept: [],
804
- };
805
- const clicks = [];
806
- const click = (id, to, tip) => {
807
- if (to && (to.startsWith('#') || existsSync(to))) {
808
- const url = to.startsWith('#')
809
- ? to
810
- : encodeURI(relative(t.dir, to).split(sep).join('/'));
811
- clicks.push(` click ${id} href "${url}" "${tip}"`);
812
- }
945
+ const { applyAt, mergeEvery } = t.config;
946
+ const applying = q.applying;
947
+ const took = q.current ? since(Date.now() - q.current.since) : '';
948
+ const waiting = m.unmerged();
949
+ const merged = m.merges().filter((x) => x.ok);
950
+ const running = t.workers.filter((w) => w.case && w.step === 'run').length;
951
+ const grading = t.workers.filter((w) => w.case && w.step === 'analyze').length;
952
+ const rechecking = t.workers.filter((w) => w.case && w.recheck).length;
953
+ const rechecksQueued = t.rechecks.all().filter((r) => r.state === 'queued').length;
954
+ const paint = {};
955
+ const fill = (id, cls) => {
956
+ (paint[cls] ??= []).push(id);
813
957
  };
958
+ const { clicks, click } = clickable(t);
814
959
  const out = [
815
- '```mermaid',
816
- '---',
817
- 'config:',
818
- ' flowchart:',
819
- ' wrappingWidth: 480',
820
- '---',
960
+ ...DIAGRAM_HEAD,
821
961
  'flowchart TB',
822
- ` DS[("dataset · ${counts.queued} queued")] --> D["${lines('dispatcher', 'seats a case on a free worker', 'woken cases first')}"]`,
962
+ ` DS[("${lines('**cases**', `${counts.queued} not started`)}")]`,
963
+ ` DS -->|"next case"| WORKERS`,
823
964
  ` subgraph WORKERS["workers · ${t.busy()} of ${t.workers.length} busy"]`,
824
965
  ' direction LR',
825
966
  ];
826
967
  click('DS', join(t.root, DATASET_DIR), 'the dataset');
827
- click('D', join(t.dir, 'events.jsonl'), 'every step, in order');
828
- const lanes = [];
968
+ const paused = applying ? 'paused · fixes' : t.drained ? 'nothing left to start' : 'idle';
829
969
  for (const w of t.workers) {
830
- const n = w.slot;
831
- const step = w.case ? w.step : 'idle';
832
- let label = idleWord(t);
833
- if (w.case) {
834
- const tries = w.phase === 'mem' ? t.config.memTries : t.config.tries;
835
- label =
836
- w.attempt === undefined
837
- ? lines(`**${safe(w.case.id, 48)}**`, 'starting')
838
- : lines(`**${safe(w.case.id, 48)}**`, `${PHASE_WORD[w.phase ?? 'nomem']} · try ${w.attempt} of ${tries} · ${step} ${since(Date.now() - w.since)}`);
839
- click(`C${n}`, feedbackPath(t, w.case), 'the case: rubric, tries, decisions');
840
- if (w.attempt !== undefined) {
841
- const dir = t.attemptDir(w.case, w.phase ?? 'nomem', w.attempt);
842
- const run = readJson(join(dir, 'run.json'));
843
- if (run) {
844
- click(`R${n}`, run.report ?? run.dir, 'the run report');
845
- }
846
- else {
847
- click(`R${n}`, join(dir, 'run.log'), 'the run log, live');
848
- }
849
- if (existsSync(join(dir, 'analysis.md'))) {
850
- click(`A${n}`, join(dir, 'analysis.md'), 'the analysis');
851
- }
852
- else {
853
- click(`A${n}`, join(dir, 'analyze.log'), 'the analysis log, live');
854
- }
855
- }
856
- }
857
- out.push(` subgraph W${n}["worker ${n}"]`, ' direction TB', ` C${n}["${label}"] --> R${n}["run"] --> A${n}["analyze"]`, ' end');
858
- lit[w.case ? 'case' : 'empty'].push(`C${n}`);
859
- lit[step === 'run' ? 'run' : 'off'].push(`R${n}`);
860
- lit[step === 'analyze' ? 'analyze' : 'off'].push(`A${n}`);
861
- lanes.push(` style W${n} ${LANE[step]}`);
862
- }
863
- // Edges go to the box, not its nodes, so it keeps LR and the chain puts the lanes in slot order.
864
- out.push(...inOrder('W', t.workers.length), ' end', ' D --> WORKERS',
865
- // Not a diamond: an edge to a box ends at the node's bounding box, short of a diamond's sides.
866
- ' WORKERS --> V(["feedback done?"])', ` V -.->|"${lines('passed without memory:', 'again with memory')}"| WORKERS`, ` V -->|"${lines('failed, tries left:', 'park, free the worker')}"| Q`, ` V -->|"no tries left"| X["difficult · ${counts.difficult}"]`, ` V -->|"passed with memory"| CD["completed · ${counts.completed}"]`, ` subgraph Q["improvement queue · ${q.pending.length} of ${t.config.applyAt}"]`, ' direction LR');
867
- click('X', '#difficult-cases', 'the difficult cases');
868
- click('CD', '#cases', 'every case');
869
- const slots = Math.max(t.config.applyAt, q.pending.length);
870
- for (let i = 0; i < slots; i++) {
871
- const f = q.pending[i];
872
- out.push(f
873
- ? ` Q${i + 1}["${lines(`**${safe(f.case, 40)}**`, `${PHASE_WORD[f.phase]} try ${f.attempt} · ${f.improvements.length} improvement(s)`)}"]`
874
- : ` Q${i + 1}["empty"]`);
875
- lit[f ? 'full' : 'empty'].push(`Q${i + 1}`);
876
- if (f) {
877
- const dir = t.attemptDir({ id: f.case, rev: f.caseRev }, f.phase, f.attempt);
878
- click(`Q${i + 1}`, join(dir, 'analysis.md'), 'the analysis asking for these');
970
+ const id = `W${w.slot}`;
971
+ if (!w.case) {
972
+ out.push(` ${id}("${lines(`**worker ${w.slot}**`, paused)}")`);
973
+ fill(id, 'idle');
974
+ continue;
879
975
  }
976
+ const tries = w.recheck
977
+ ? RECHECK_TRIES
978
+ : w.phase === 'mem'
979
+ ? t.config.memTries
980
+ : t.config.tries;
981
+ const started = w.attempt !== undefined;
982
+ const what = w.recheck
983
+ ? `recheck after ${w.recheck.apply}`
984
+ : PHASE_WORD[w.phase ?? 'nomem'];
985
+ out.push(` ${id}("${lines(`**worker ${w.slot}** · ${started ? `${STEP_WORD[w.step]} ${since(Date.now() - w.since)}` : 'starting'}`, safe(w.case.id, 36), started ? `${what} · try ${w.attempt} of ${tries}` : '', started ? liveLabel(nowOf(t, w)) : '')}")`);
986
+ fill(id, w.step === 'run' ? 'run' : w.step === 'analyze' ? 'analyze' : 'case');
987
+ click(id, w.recheck ? w.recheck.dir : feedbackPath(t, w.case), w.recheck ? 'this recheck try' : 'the case: rubric, tries, decisions');
880
988
  }
881
- out.push(...inOrder('Q', slots));
882
- const last = q.applies().at(-1);
883
- const applier = !q.applying
884
- ? `idle${last ? ` · last ${last.name}` : ''}`
885
- : q.inApply.length > 0
886
- ? `applying ${q.inApply.length} request(s) · no run starts`
887
- : 'waiting for runs · no run starts';
888
- out.push(' end',
889
- // AP's own nodes link outside, so Mermaid cannot end an edge on the AP box itself.
890
- ' Q -->|"full, or nothing left to start"| P1', ` subgraph AP["applier · ${applier}"]`, ` P1["wait for runs in flight · ${q.runs}"] --> P2["${lines('meta run /finetune-apply', 'then zen check')}"]`, ` P2 --> P3["${lines('applied.json', 'wake the batch')}"]`, ' end', ` P2 -->|"writes the next version"| S[("${lines(`system prose · v${t.system.version()}`, 'read by every new run')}")]`, ' P3 -->|"wake"| D');
891
- const active = !q.applying ? undefined : q.runs > 0 ? 'P1' : 'P2';
892
- for (const p of ['P1', 'P2', 'P3']) {
893
- lit[p === active ? 'busy' : 'off'].push(p);
894
- }
895
- if (last) {
896
- click('P2', applyPath(last.dir), q.applying ? 'this apply' : 'the last apply');
897
- click('P3', join(last.dir, 'diff.patch'), 'what the last apply changed');
898
- }
899
- click('S', t.system.snapshotDir(t.system.version()), 'this version of the prose');
900
- const waiting = m.unmerged();
901
- const kept = Math.max(t.config.mergeEvery, waiting.length);
902
- out.push(' CD -->|"keep its memory"| M', ` subgraph M["memory to merge · ${waiting.length} of ${t.config.mergeEvery}"]`, ' direction LR');
903
- for (let i = 0; i < kept; i++) {
904
- const s = waiting[i];
905
- out.push(` K${i + 1}["${s ? safe(s.case, 40) : 'empty'}"]`);
906
- lit[s ? 'kept' : 'empty'].push(`K${i + 1}`);
907
- if (s) {
908
- click(`K${i + 1}`, s.dir, 'the kept memory');
909
- }
989
+ const queued = q.pending
990
+ .slice(0, 8)
991
+ .map((f) => `${safe(f.case, 32)} · ${plural(f.improvements.length, 'fix', 'fixes')}`);
992
+ if (q.pending.length > 8) {
993
+ queued.push(`and ${q.pending.length - 8} more`);
994
+ }
995
+ const applier = applying
996
+ ? q.runs > 0
997
+ ? `waiting for ${plural(q.runs, 'run')} to finish`
998
+ : [
999
+ `${plural(q.inApply.length, 'case')} · ${took}`,
1000
+ liveLabel(applierNow(t)) || (q.current?.step === 'check' ? 'zen check' : ''),
1001
+ ]
1002
+ .filter(Boolean)
1003
+ .join('\n')
1004
+ : `idle${last ? ` · last ${last.name}` : ''}`;
1005
+ out.push(...inOrder('W', t.workers.length), ' end', ' WORKERS --> V(["graded"])', ` V -.->|"${lines('passed without memory:', 'again with memory')}"| WORKERS`, ` V -->|"out of tries"| X("difficult · ${counts.difficult}")`, ` V -->|"${lines('failed, tries left,', 'or a recheck failed')}"| Q`, ` V -->|"passed with memory"| CD`, ' subgraph LEARN["fixing the instructions"]', ` Q("${lines(`**waiting for a fix · ${q.pending.length} of ${applyAt}**`, ...(queued.length ? queued : ['empty']))}")`, ` Q -->|"${lines(`${applyAt} waiting, or`, 'nothing else to run')}"| P("${lines('**apply fixes**', 'an agent edits the instructions', 'and tidies what it touched', applier)}")`, ` P -->|"new version"| S[("${lines(`**instructions · v${t.system.version()}**`, 'every new run reads these')}")]`, ' end', ...(t.config.recheck > 0
1006
+ ? [
1007
+ ` S -.->|"${lines(`recheck ${t.config.recheck} cases that passed`, `${rechecksQueued} queued`)}"| WORKERS`,
1008
+ ]
1009
+ : []));
1010
+ fill('Q', q.pending.length > 0 ? 'full' : 'empty');
1011
+ fill('P', applying ? 'busy' : 'off');
1012
+ click('Q', anchor(`Improvement queue - ${q.pending.length} of ${applyAt} parked`), 'every case waiting, with what it asks for');
1013
+ const applied = applying && q.current?.dir ? q.current.dir : last?.dir;
1014
+ if (applied) {
1015
+ click('P', applyPath(applied), applying ? 'this apply' : 'the last apply');
1016
+ }
1017
+ click('S', t.system.snapshotDir(t.system.version()), 'this version of the instructions');
1018
+ const kept = waiting.slice(0, 6).map((s) => safe(s.case, 32));
1019
+ if (waiting.length > 6) {
1020
+ kept.push(`and ${waiting.length - 6} more`);
1021
+ }
1022
+ out.push(' subgraph KEEP["keeping memory"]', ` CD("completed · ${counts.completed}") -->|"keep its memory"| M("${lines(`**memory to merge · ${waiting.length} of ${mergeEvery}**`, ...(kept.length ? kept : ['empty']))}")`, ` M -->|"${lines(`${mergeEvery} waiting, or`, 'the tuning ends')}"| MG[("${lines('**merged memory**', plural(merged.length, 'graph'))}")]`, ' end');
1023
+ fill('M', waiting.length > 0 ? 'kept' : 'empty');
1024
+ if (m.submissions().length > 0 || m.merges().length > 0) {
1025
+ click('M', '#memory', 'every kept memory and merge');
910
1026
  }
911
- const merged = m.merges().filter((x) => x.ok);
912
1027
  click('MG', merged.at(-1)?.dir, 'the latest merged graph');
913
- out.push(...inOrder('K', kept), ' end', ` M -->|"full, or tuning ends"| MG[("merged graphs · ${merged.length}")]`, ...clicks, ' classDef case fill:#ffffff,stroke:#57606a,color:#24292f', ' classDef run fill:#0969da,stroke:#0550ae,color:#ffffff', ' classDef analyze fill:#8250df,stroke:#6639ba,color:#ffffff', ' classDef off fill:#ffffff,stroke:#d0d7de,color:#8c959f', ' classDef full fill:#d4a72c,stroke:#9a6700,color:#ffffff', ' classDef empty fill:#ffffff,stroke:#d0d7de,stroke-dasharray:4 3,color:#8c959f', ' classDef busy fill:#e16f24,stroke:#bc4c00,color:#ffffff', ' classDef kept fill:#1b7c83,stroke:#136061,color:#ffffff', ' classDef done fill:#1a7f37,stroke:#116329,color:#ffffff', ' classDef bad fill:#cf222e,stroke:#a40e26,color:#ffffff', ' classDef store fill:#ddf4ff,stroke:#54aeff,color:#0a3069', ' classDef hub fill:#eaeef2,stroke:#57606a,color:#24292f', ...Object.entries(lit)
914
- .filter(([, ids]) => ids.length > 0)
915
- .map(([cls, ids]) => ` class ${ids.join(',')} ${cls}`), ' class CD done', ' class X bad', ' class DS,S,MG store', ' class D,V hub', ' style WORKERS fill:#ffffff,stroke:#8c959f', ...lanes, ` style Q fill:#fff8c5,stroke:#d4a72c${q.pending.length >= t.config.applyAt ? ',stroke-width:3px' : ''}`, ` style AP ${q.applying ? 'fill:#fff1e5,stroke:#e16f24,stroke-width:3px' : 'fill:#f6f8fa,stroke:#d0d7de'}`, ' style M fill:#e6f6f5,stroke:#1b7c83', '```');
916
- const running = lit.run.length;
917
- const analyzing = lit.analyze.length;
918
- const now = [
919
- running > 0 && `- **Blue** - ${running} worker(s) running a case`,
920
- analyzing > 0 && `- **Purple** - ${analyzing} worker(s) analyzing the run just finished`,
921
- q.applying &&
922
- '- **Orange** - fixes are being applied to the agent instructions; no new run starts until that is done',
923
- q.pending.length > 0 &&
924
- `- **Yellow** - ${q.pending.length} failed case(s) waiting for fixes; they are applied when ${t.config.applyAt} wait, or when no other case can run`,
925
- waiting.length > 0 &&
926
- `- **Teal** - memory from ${waiting.length} passed case(s) waiting to be merged; merged when ${t.config.mergeEvery} wait`,
927
- ].filter((x) => typeof x === 'string');
1028
+ if (counts.difficult > 0) {
1029
+ click('X', '#difficult-cases', 'the difficult cases');
1030
+ }
1031
+ click('CD', '#cases', 'every case');
1032
+ out.push(...clicks, ...PALETTE, ...Object.entries(paint).map(([cls, ids]) => ` class ${ids.join(',')} ${cls}`), ' class CD done', ' class X bad', ' class DS,S,MG store', ' class V hub', ' style WORKERS fill:#ffffff,stroke:#d0d7de', ' style LEARN fill:#fffdf5,stroke:#e6d9a8', ' style KEEP fill:#f5fbfa,stroke:#a8d8d3', ...(q.pending.length >= applyAt ? [' style Q stroke-width:3px'] : []), '```');
1033
+ const now = [];
1034
+ const workers = t.workers.length;
1035
+ if (applying) {
1036
+ now.push(q.runs > 0
1037
+ ? `- **About to write fixes into the instructions** (orange): waiting for the ${plural(q.runs, 'run')} in progress to finish, so none of them reads half-edited instructions. No new case starts meanwhile.`
1038
+ : `- **Writing fixes into the instructions** (orange) for ${plural(q.inApply.length, 'case')} - running for ${took}. No case starts until it is done; then those cases run again on the new version, and ${plural(t.config.recheck, 'case')} that passed before are rechecked on it.`);
1039
+ }
1040
+ if (running + grading > 0) {
1041
+ const doing = [
1042
+ running > 0 ? `${running} running a case (blue)` : '',
1043
+ grading > 0 ? `${grading} grading a finished run (purple)` : '',
1044
+ ].filter(Boolean);
1045
+ now.push(`- **${t.busy()} of ${workers} workers busy**: ${doing.join(', ')}${rechecking > 0 ? ` - ${rechecking} of them rechecking a case that passed before` : ''}.`);
1046
+ }
1047
+ if (rechecksQueued > 0) {
1048
+ now.push(`- **${plural(rechecksQueued, 'recheck')} queued**: cases that passed, to run again on the latest instructions before any new case starts.`);
1049
+ }
1050
+ if (q.pending.length > 0) {
1051
+ now.push(`- **${plural(q.pending.length, 'case')} waiting for a fix** (yellow). Their fixes are written once ${applyAt} are waiting, or when nothing else is left to run${applying ? ' - in the round after this one' : ''}.`);
1052
+ }
1053
+ if (counts.queued > 0) {
1054
+ now.push(`- **${plural(counts.queued, 'case')} not started yet.**`);
1055
+ }
1056
+ if (waiting.length > 0) {
1057
+ now.push(`- **${plural(waiting.length, 'finished case')} with memory to merge** (teal); memories are merged ${mergeEvery} at a time.`);
1058
+ }
928
1059
  return [
929
- ...(now.length > 0 ? ['Now:', '', ...now] : ['Nothing is in progress right now.']),
1060
+ ...(now.length > 0 ? ['**Right now**', '', ...now] : ['Nothing is running right now.']),
1061
+ '',
1062
+ '<details>',
1063
+ '<summary>How this works</summary>',
1064
+ '',
1065
+ `1. A worker takes a case and runs the agents on it **without memory**; then another agent grades the run against the case's rubric.`,
1066
+ `2. **Failed:** the grader suggests fixes to the agents' instructions. The case waits for a fix (yellow) and its worker takes another case.`,
1067
+ `3. Once ${applyAt} cases are waiting, or nothing else is left to run, one agent writes their fixes into the instructions - an **apply** (orange) - and tidies up the files it touched: repeated rules merged, contradictions cut. No case starts while the instructions change; afterwards the waiting cases run again on the new version.`,
1068
+ ...(t.config.recheck > 0
1069
+ ? [
1070
+ `4. After each apply, ${plural(t.config.recheck, 'case')} that passed before **run again** on the new version, as ordinary work - a recheck. One that fails ${RECHECK_TRIES} tries has regressed: its fixes wait for the next apply like any other.`,
1071
+ ]
1072
+ : []),
1073
+ `${t.config.recheck > 0 ? 5 : 4}. **Passed:** the case runs again **with memory**. When that passes too, the case is completed and its memory is kept; kept memories are merged ${mergeEvery} at a time (teal).`,
1074
+ `${t.config.recheck > 0 ? 6 : 5}. A case still failing after ${plural(t.config.tries, 'try', 'tries')} without memory, or ${t.config.memTries} with, is marked **difficult**.`,
1075
+ '',
1076
+ '</details>',
930
1077
  '',
931
1078
  'Click a box to open it.',
932
1079
  '',
@@ -965,10 +1112,12 @@ function summarySection(spent, tuning, memory) {
965
1112
  }
966
1113
  return out;
967
1114
  }
968
- export function writeStatus(t) {
1115
+ function slowStatus(t) {
969
1116
  const here = t.dir;
1117
+ const q = t.improvements;
970
1118
  const cases = selection(t);
971
1119
  const store = DatasetStore.open(t.root);
1120
+ const byCase = new Map();
972
1121
  const counts = {
973
1122
  queued: 0,
974
1123
  nomem: 0,
@@ -980,6 +1129,7 @@ export function writeStatus(t) {
980
1129
  };
981
1130
  const rows = [];
982
1131
  const difficult = [];
1132
+ const rubricProblems = [];
983
1133
  const failed = [];
984
1134
  let everything = {};
985
1135
  const spent = {
@@ -988,6 +1138,24 @@ export function writeStatus(t) {
988
1138
  };
989
1139
  const tuning = { passed: 0, first: 0, before: blank(), after: blank() };
990
1140
  const memory = { cases: 0, cheaper: 0, before: blank(), after: blank() };
1141
+ const lastRun = (a) => {
1142
+ if (!a) {
1143
+ return '-';
1144
+ }
1145
+ const links = [
1146
+ a.run ? link('run', here, a.run.dir) : '',
1147
+ a.run?.report ? link('report', here, a.run.report) : '',
1148
+ existsSync(join(a.dir, 'run.log')) ? link('run log', here, join(a.dir, 'run.log')) : '',
1149
+ existsSync(join(a.dir, 'analysis.md'))
1150
+ ? link('analysis', here, join(a.dir, 'analysis.md'))
1151
+ : '',
1152
+ existsSync(join(a.dir, 'analyze.log'))
1153
+ ? link('analyze log', here, join(a.dir, 'analyze.log'))
1154
+ : '',
1155
+ ].filter(Boolean);
1156
+ return `${PHASE_WORD[a.phase]} try ${a.attempt}: ${links.join(' · ') || '-'}`;
1157
+ };
1158
+ const stamp = (iso) => `${iso.slice(0, 19).replace('T', ' ')} UTC`;
991
1159
  for (const c of cases) {
992
1160
  const w = t.workers.find((x) => x.case?.id === c.id);
993
1161
  const parked = t.improvements.pending.find((f) => f.case === c.id) ??
@@ -1015,6 +1183,7 @@ export function writeStatus(t) {
1015
1183
  const calls = nomem.map((a) => a.run?.turns).filter((n) => n !== undefined);
1016
1184
  const memCalls = mem.map((a) => a.run?.turns).filter((n) => n !== undefined);
1017
1185
  const caseBy = caseTokens(t, c);
1186
+ byCase.set(`${c.id}@${c.rev}`, caseBy);
1018
1187
  everything = mergeStages(everything, caseBy);
1019
1188
  for (const a of attempts) {
1020
1189
  const f = runFlat(a.run);
@@ -1050,112 +1219,169 @@ export function writeStatus(t) {
1050
1219
  : '-';
1051
1220
  rows.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${cell(c.class)} | ${c.rubric.length} | ${cell(stateOf(t, c))} | ${nomem.length} / ${mem.length} | ${cell(last?.feedback?.verdict)} ${rubricScore(last?.feedback)} | ${calls.length ? `${calls[0]} → ${calls.at(-1)}` : '-'} | ${memCalls.length ? memCalls.at(-1) : '-'} | ${effect} | ${inOut(caseBy.run)} | ${inOut(caseBy.analyze)} | ${attempts.at(-1)?.run ? `v${attempts.at(-1).run.system}` : '-'} |`);
1052
1221
  if (r?.state === 'difficult') {
1053
- difficult.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${PHASE_WORD[r.phase]} | ${cell(r.reason)} | ${r.phase === 'nomem' ? nomem.length : mem.length} | ${cell(last?.feedback?.summary)} | ${last && existsSync(join(last.dir, 'analysis.md')) ? link('last analysis', here, join(last.dir, 'analysis.md')) : '-'} |`);
1222
+ difficult.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${PHASE_WORD[r.phase]} | ${cell(r.reason)} | ${r.phase === 'nomem' ? nomem.length : mem.length} | ${stamp(r.at)} | ${cell(lead(last?.feedback?.summary ?? ''))} | ${lastRun(attempts.at(-1))} |`);
1223
+ }
1224
+ const broken = Object.entries(last?.feedback?.rubric ?? {})
1225
+ .filter(([, g]) => g === 'invalid')
1226
+ .map(([id]) => id);
1227
+ if (broken.length > 0) {
1228
+ const why = (last?.feedback?.improvements ?? []).filter(forDataset);
1229
+ rubricProblems.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${broken.join(', ')} | ${cell(why.map((i) => i.change).join(' / '))} |`);
1054
1230
  }
1055
1231
  if (r?.state === 'failed') {
1056
- failed.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${PHASE_WORD[r.phase]}, try ${(r.phase === 'nomem' ? nomem : mem).length || 1} | ${cell(r.reason)} | ${r.at.slice(11, 16)} |`);
1232
+ failed.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${PHASE_WORD[r.phase]}, try ${(r.phase === 'nomem' ? nomem : mem).length || 1} | ${cell(r.reason)} | ${stamp(r.at)} | ${lastRun(attempts.at(-1))} |`);
1057
1233
  }
1058
1234
  }
1235
+ const later = [];
1236
+ const applies = q.applies();
1237
+ if (applies.length > 0) {
1238
+ later.push('## Applies', '', 'Each apply links to its own page: every request, every decision, every file changed and why. Prose is the change in what every call reads, in bytes, summed over the agents.', '', '| Apply | Versions | At | Requests | Applied | Merged | Rejected | Deferred | Structural | Prose | Files changed | Tokens | Details |', '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |');
1239
+ for (const a of [...applies].reverse()) {
1240
+ const d = readJson(join(a.dir, 'decisions.json')) ?? {};
1241
+ const tally = {};
1242
+ for (const byImprovement of Object.values(d)) {
1243
+ for (const x of Object.values(byImprovement)) {
1244
+ tally[x.decision ?? '?'] = (tally[x.decision ?? '?'] ?? 0) + 1;
1245
+ }
1246
+ }
1247
+ const patch = join(a.dir, 'diff.patch');
1248
+ const files = existsSync(patch)
1249
+ ? (readFileSync(patch, 'utf8').match(/^diff --git /gm) ?? []).length
1250
+ : 0;
1251
+ const details = [
1252
+ existsSync(join(a.dir, 'changes.md'))
1253
+ ? link('changes', here, join(a.dir, 'changes.md'))
1254
+ : '',
1255
+ existsSync(patch) ? link('diff', here, patch) : '',
1256
+ existsSync(join(a.dir, 'decisions.json'))
1257
+ ? link('decisions', here, join(a.dir, 'decisions.json'))
1258
+ : '',
1259
+ ].filter(Boolean);
1260
+ const versions = a.applied
1261
+ ? a.applied.discarded
1262
+ ? `undone at v${a.applied.from}`
1263
+ : `v${a.applied.from} → v${a.applied.to}`
1264
+ : existsSync(join(a.dir, 'failed.json'))
1265
+ ? 'failed'
1266
+ : 'in progress';
1267
+ const prose = a.applied?.bytes
1268
+ ? `${a.applied.bytes.after - a.applied.bytes.before > 0 ? '+' : ''}${a.applied.bytes.after - a.applied.bytes.before}`
1269
+ : '-';
1270
+ const what = a.applied?.kind === 'consolidate' || existsSync(join(a.dir, 'consolidate.json'))
1271
+ ? `${link(a.name, here, applyPath(a.dir))} consolidation`
1272
+ : link(a.name, here, applyPath(a.dir));
1273
+ later.push(`| ${what} | ${versions} | ${a.applied?.at.slice(11, 16) ?? '-'} | ${a.inputs?.length ?? 0} | ${tally.applied ?? 0} | ${tally.merged ?? 0} | ${tally.rejected ?? 0} | ${tally.deferred ?? 0} | ${tally.structural ?? 0} | ${prose} | ${files} | ${inOut(applyTokens(a.dir))} | ${details.join(' · ') || (a.applied?.skipped ? 'nothing to change' : '-')} |`);
1274
+ }
1275
+ later.push('');
1276
+ }
1277
+ later.push(...recheckSection(t, here, () => true, 30));
1278
+ const allStages = mergeStages(everything, t.rechecks.tokens(), {
1279
+ apply: mergeModels(...applies.map((a) => applyTokens(a.dir))),
1280
+ });
1281
+ t.progress = { counts, total: cases.length, spent: allStages };
1282
+ const rest = [...memorySection(t, here)];
1283
+ rest.push('## Cases', '', '| Case | Class | Rubric | State | Tries without / with | Last verdict | LLM calls without memory | With memory | Memory effect | Run tokens | Analyze tokens | System |', '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |', ...rows, '');
1284
+ if (difficult.length > 0) {
1285
+ rest.push('## Difficult cases', '', '| Case | Phase | Reason | Tries | Difficult since | Last summary | Last run |', '| --- | --- | --- | --- | --- | --- | --- |', ...difficult, '');
1286
+ }
1287
+ if (rubricProblems.length > 0) {
1288
+ rest.push('## Rubric problems', '', 'Lines the latest analysis graded `invalid`: they say how one implementation did it, not what a right answer is. No apply touches them - fix the case with `zen meta dataset`.', '', '| Case | Lines | What is wrong |', '| --- | --- | --- |', ...rubricProblems, '');
1289
+ }
1290
+ if (failed.length > 0) {
1291
+ rest.push('## Failed cases', '', 'Something outside the prose broke - the sandbox, a provider, a key - so these taught nothing. They keep their tries and carry on from the step that failed at the next `zen meta finetune start`.', '', '| Case | Stopped at | Reason | Failed at | Last run |', '| --- | --- | --- | --- | --- |', ...failed, '');
1292
+ }
1293
+ return {
1294
+ total: cases.length,
1295
+ revision: store.manifest.revision,
1296
+ counts,
1297
+ summary: summarySection(spent, tuning, memory),
1298
+ applies: applies.length,
1299
+ lastApply: applies.at(-1),
1300
+ byCase,
1301
+ spent: allStages,
1302
+ later,
1303
+ rest,
1304
+ };
1305
+ }
1306
+ const slowParts = new WeakMap();
1307
+ /** `live` reuses the parts read from every case and apply; only what moves by the second is read. */
1308
+ export function writeStatus(t, live = false) {
1309
+ const here = t.dir;
1310
+ let s = live ? slowParts.get(t) : undefined;
1311
+ if (!s) {
1312
+ s = slowStatus(t);
1313
+ slowParts.set(t, s);
1314
+ }
1315
+ const counts = s.counts;
1059
1316
  const done = counts.completed + counts.difficult;
1060
- const bar = cases.length ? Math.round((done / cases.length) * 20) : 0;
1317
+ const bar = s.total ? Math.round((done / s.total) * 20) : 0;
1061
1318
  const state = t.finished ? 'finished' : t.stopping ? 'stopping' : 'running';
1062
1319
  const q = t.improvements;
1063
1320
  const out = [
1064
1321
  `# Fine-tuning - ${basename(t.root)}`,
1065
1322
  '',
1066
1323
  `**${state}** · started ${new Date(t.startedAt).toISOString().slice(0, 16).replace('T', ' ')} · elapsed ${since(Date.now() - t.startedAt)} · updated ${new Date().toISOString().slice(11, 19)}`,
1067
- `system **v${t.system.version()}** (applies: ${q.applies().length}) · dataset rev **${store.manifest.revision}** · workers **${t.workers.filter((w) => w.case).length} / ${t.workers.length}** · apply at **${t.config.applyAt}** · tries ${t.config.tries} without memory, ${t.config.memTries} with`,
1324
+ `system **v${t.system.version()}** (applies: ${s.applies}) · dataset rev **${s.revision}** · workers **${t.workers.filter((w) => w.case).length} / ${t.workers.length}** · apply at **${t.config.applyAt}** · tries ${t.config.tries} without memory, ${t.config.memTries} with`,
1068
1325
  '',
1069
- `\`${'█'.repeat(bar)}${'░'.repeat(20 - bar)}\` ${done} of ${cases.length} cases done - ${counts.completed} completed, ${counts.difficult} difficult${counts.failed ? ` · ${counts.failed} failed, they resume at the next start` : ''}`,
1326
+ `\`${'█'.repeat(bar)}${'░'.repeat(20 - bar)}\` ${done} of ${s.total} cases done - ${counts.completed} completed, ${counts.difficult} difficult${counts.failed ? ` · ${counts.failed} failed, they resume at the next start` : ''}`,
1070
1327
  '',
1071
- ...summarySection(spent, tuning, memory),
1328
+ ...s.summary,
1072
1329
  '## Overview',
1073
1330
  '',
1074
- ...overviewDiagram(counts),
1331
+ ...pathDiagram(t, counts),
1075
1332
  '',
1076
1333
  '## System',
1077
1334
  '',
1078
- ...systemDiagram(t, counts),
1335
+ ...systemDiagram(t, counts, s.lastApply),
1079
1336
  '',
1080
1337
  '## Workers',
1081
1338
  '',
1082
- ...t.workers.flatMap((w) => [
1083
- `### Worker ${w.slot}${w.case ? ` - ${link(w.case.id, here, feedbackPath(t, w.case))}` : ''}`,
1084
- '',
1085
- ...workerDiagram(t, w),
1086
- '',
1087
- ...(w.case ? tokenTable(caseTokens(t, w.case)) : []),
1088
- ]),
1089
- '| Worker | Case | Class | Phase | Try | Step | Since | Run | Analysis | Session |',
1090
- '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
1339
+ '| Worker | Case | Class | Phase | Try | Step | Since | Run | Analysis | Session | Now |',
1340
+ '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
1091
1341
  ];
1092
1342
  for (const w of t.workers) {
1093
1343
  if (!w.case) {
1094
- out.push(`| ${w.slot} | - | - | - | - | ${idleWord(t)} | ${since(Date.now() - w.since)} | - | - | - |`);
1344
+ out.push(`| ${w.slot} | - | - | - | - | ${idleWord(t)} | - | - | - | - | - |`);
1095
1345
  continue;
1096
1346
  }
1097
- const dir = t.attemptDir(w.case, w.phase ?? 'nomem', w.attempt ?? 1);
1347
+ const dir = t.workDir({ ...w, case: w.case });
1098
1348
  const run = readJson(join(dir, 'run.json'));
1099
- const session = existsSync(join(t.caseDir(w.case), 'session'))
1100
- ? readFileSync(join(t.caseDir(w.case), 'session'), 'utf8').trim()
1101
- : undefined;
1102
- out.push(`| ${w.slot} | ${link(w.case.id, here, feedbackPath(t, w.case))} | ${cell(w.case.class)} | ${PHASE_WORD[w.phase ?? 'nomem']} | ${w.attempt} / ${w.phase === 'mem' ? t.config.memTries : t.config.tries} | ${w.step} | ${since(Date.now() - w.since)} | ${run ? `${link('run', here, run.dir)}${run.report ? ` · ${link('report', here, run.report)}` : ''}` : '-'} | ${existsSync(join(dir, 'analysis.md')) ? link('analysis', here, join(dir, 'analysis.md')) : '-'} | ${session ? `\`zen meta resume ${basename(t.root)} ${session}\`` : '-'} |`);
1349
+ const session = analyzeSession(dir);
1350
+ const phase = w.recheck
1351
+ ? `recheck after ${w.recheck.apply}`
1352
+ : PHASE_WORD[w.phase ?? 'nomem'];
1353
+ const tries = w.recheck
1354
+ ? RECHECK_TRIES
1355
+ : w.phase === 'mem'
1356
+ ? t.config.memTries
1357
+ : t.config.tries;
1358
+ out.push(`| ${w.slot} | ${link(w.case.id, here, feedbackPath(t, w.case))} | ${cell(w.case.class)} | ${phase} | ${w.attempt} / ${tries} | ${w.step} | ${since(Date.now() - w.since)} | ${run ? `${link('run', here, run.dir)}${run.report ? ` · ${link('report', here, run.report)}` : ''}` : '-'} | ${existsSync(join(dir, 'analysis.md')) ? link('analysis', here, join(dir, 'analysis.md')) : '-'} | ${session ? `\`zen meta resume ${basename(t.root)} ${session}\`` : '-'} | ${liveCells(liveRow(t, w)?.recent)} |`);
1359
+ }
1360
+ for (const w of t.workers) {
1361
+ const by = w.case
1362
+ ? tokenTable(mergeStages(s.byCase.get(`${w.case.id}@${w.case.rev}`) ?? caseTokens(t, w.case), inFlight(liveRow(t, w))))
1363
+ : [];
1364
+ if (w.case && by.length > 0) {
1365
+ out.push('', `### Worker ${w.slot} - ${link(w.case.id, here, feedbackPath(t, w.case))}`, '', ...by);
1366
+ }
1103
1367
  }
1104
1368
  out.push('', `## Improvement queue - ${q.pending.length} of ${t.config.applyAt} parked`, '');
1105
1369
  out.push(q.applying
1106
- ? 'Applying now: no new run starts until it is done.'
1370
+ ? `Applying now: no new run starts until it is done.${applierNow(t) ? ` Now: ${liveCell(applierNow(t))}` : ''}`
1107
1371
  : `A parked case gives its worker back, so another case runs meanwhile. The next apply comes when ${Math.max(0, t.config.applyAt - q.pending.length)} more park, or once nothing is running and no case is left to start; woken cases take a worker before new ones.`, '');
1108
1372
  if (q.pending.length > 0) {
1109
1373
  out.push('| Request | Case | Phase | Try | Improvements | Summary |', '| --- | --- | --- | --- | --- | --- |');
1110
1374
  for (const f of q.pending) {
1111
- out.push(`| ${cell(f.id)} | ${link(f.case, here, feedbackPath(t, { id: f.case }))} | ${PHASE_WORD[f.phase]} | ${f.attempt} | ${f.improvements.length} | ${cell(f.summary)} |`);
1112
- }
1113
- out.push('');
1114
- }
1115
- const applies = q.applies();
1116
- if (applies.length > 0) {
1117
- out.push('## Applies', '', 'Each apply links to its own page: every request, every decision, every file changed and why.', '', '| Apply | Versions | At | Requests | Applied | Merged | Rejected | Structural | Files changed | Tokens | Details |', '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |');
1118
- for (const a of [...applies].reverse()) {
1119
- const d = readJson(join(a.dir, 'decisions.json')) ?? {};
1120
- const tally = {};
1121
- for (const byImprovement of Object.values(d)) {
1122
- for (const x of Object.values(byImprovement)) {
1123
- tally[x.decision ?? '?'] = (tally[x.decision ?? '?'] ?? 0) + 1;
1124
- }
1125
- }
1126
- const patch = join(a.dir, 'diff.patch');
1127
- const files = existsSync(patch)
1128
- ? (readFileSync(patch, 'utf8').match(/^diff --git /gm) ?? []).length
1129
- : 0;
1130
- const details = [
1131
- existsSync(join(a.dir, 'changes.md'))
1132
- ? link('changes', here, join(a.dir, 'changes.md'))
1133
- : '',
1134
- existsSync(patch) ? link('diff', here, patch) : '',
1135
- existsSync(join(a.dir, 'decisions.json'))
1136
- ? link('decisions', here, join(a.dir, 'decisions.json'))
1137
- : '',
1138
- ].filter(Boolean);
1139
- out.push(`| ${link(a.name, here, applyPath(a.dir))} | ${a.applied ? `v${a.applied.from} → v${a.applied.to}` : existsSync(join(a.dir, 'failed.json')) ? 'failed' : 'in progress'} | ${a.applied?.at.slice(11, 16) ?? '-'} | ${a.inputs?.length ?? 0} | ${tally.applied ?? 0} | ${tally.merged ?? 0} | ${tally.rejected ?? 0} | ${tally.structural ?? 0} | ${files} | ${inOut(envelopesIn(a.dir, 'apply'))} | ${details.join(' · ') || (a.applied?.skipped ? 'nothing to change' : '-')} |`);
1375
+ out.push(`| ${cell(f.id)} | ${link(f.case, here, feedbackPath(t, { id: f.case }))} | ${PHASE_WORD[f.phase]} | ${f.attempt} | ${f.improvements.length} | ${cell(lead(f.summary))} |`);
1140
1376
  }
1141
1377
  out.push('');
1142
1378
  }
1143
- const allStages = mergeStages(everything, {
1144
- apply: mergeModels(...applies.map((a) => envelopesIn(a.dir, 'apply'))),
1145
- });
1146
- t.progress = { counts, total: cases.length, spent: allStages };
1147
- const tokenRows = tokenTable(allStages);
1379
+ out.push(...s.later);
1380
+ const tokenRows = tokenTable(mergeStages(s.spent, ...t.workers.map((w) => inFlight(liveRow(t, w))), inFlight(t.improvements.current?.dir ? applierRow(t) : undefined)));
1148
1381
  if (tokenRows.length > 0) {
1149
- out.push('## Tokens', '', 'Every try of every case so far, and every apply.', '', ...tokenRows);
1150
- }
1151
- out.push(...memorySection(t, here));
1152
- out.push('## Cases', '', '| Case | Class | Rubric | State | Tries without / with | Last verdict | LLM calls without memory | With memory | Memory effect | Run tokens | Analyze tokens | System |', '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |', ...rows, '');
1153
- if (difficult.length > 0) {
1154
- out.push('## Difficult cases', '', '| Case | Phase | Reason | Tries | Last summary | Details |', '| --- | --- | --- | --- | --- | --- |', ...difficult, '');
1155
- }
1156
- if (failed.length > 0) {
1157
- out.push('## Failed cases', '', 'Something outside the prose broke - the sandbox, a provider, a key - so these taught nothing. They keep their tries and carry on from the step that failed at the next `zen meta finetune start`.', '', '| Case | Stopped at | Reason | At |', '| --- | --- | --- | --- |', ...failed, '');
1382
+ out.push('## Tokens', '', 'Every try of every case so far, every recheck, and every apply - the steps under way included.', '', ...tokenRows);
1158
1383
  }
1384
+ out.push(...s.rest);
1159
1385
  out.push('## Recent events', '');
1160
1386
  for (const e of t.recent.slice(0, 20)) {
1161
1387
  const who = [
@@ -1165,120 +1391,7 @@ export function writeStatus(t) {
1165
1391
  ].filter(Boolean);
1166
1392
  out.push(`- ${e.at.slice(11, 19)} · ${[...who, e.what].join(' · ')}${e.detail ? ` - ${cell(e.detail)}` : ''}`);
1167
1393
  }
1168
- out.push(`- … everything in ${link('events.jsonl', here, join(t.dir, 'events.jsonl'))}`, '', '## Files', '', '- `cases/<id>/FEEDBACK.md` - one case: rubric, latest analysis, decisions, history', '- `cases/<id>/r<rev>/<NN>-<phase>/` - one try: `request.json`, `run.json`, `memory/`, `workspace/`, `feedback.json`, `analysis.md`, logs', '- `applies/<NNN>/APPLY.md` - one apply: requests, decisions, files changed with their diffs, zen check', '- `applies/<NNN>/` - `inputs.json`, `apply.log`, `check.log`, `changes.md`, `decisions.json`, `diff.patch`', '- `memories/` - `submissions.jsonl`, `merges.jsonl`, `submitted/<case>@r<rev>/`, `merged/mNN/`', '- `systems/` - `systems.jsonl` and a copy of `agents.yaml` + `agents/` per version', '- `events.jsonl` - every step, in order; `loop.json` - the settings', '- `journal.jsonl` - steps begun and not yet closed; a restart rolls back what a kill cut off', '- `STATUS.html`, `cases/<id>/FEEDBACK.html` - the same pages, rendered for a browser', '');
1169
- const md = `${out.join('\n')}\n`;
1170
- atomic(join(t.dir, 'STATUS.md'), md);
1171
- atomic(join(t.dir, 'STATUS.html'), markdownPage(`Fine-tuning - ${basename(t.root)}`, md, !t.finished));
1172
- }
1173
- // ---------------------------------------------------------------------------
1174
- // STATUS.html, FEEDBACK.html - the same Markdown, rendered in the browser
1175
- // ---------------------------------------------------------------------------
1176
- const MARKED_URL = 'https://cdn.jsdelivr.net/npm/marked@14/lib/marked.esm.js';
1177
- const PURIFY_URL = 'https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.es.mjs';
1178
- // As in neo's inspect page: no byte of the payload can close the script element.
1179
- const embed = (v) => JSON.stringify(v)
1180
- .replace(/</g, '\\u003c')
1181
- .replace(/>/g, '\\u003e')
1182
- .replace(/&/g, '\\u0026')
1183
- .replace(/\u2028/g, '\\u2028')
1184
- .replace(/\u2029/g, '\\u2029');
1185
- export function markdownPage(title, md, live) {
1186
- return `<!doctype html>
1187
- <html lang="en">
1188
- <head>
1189
- <meta charset="utf-8">
1190
- <meta name="viewport" content="width=device-width, initial-scale=1">
1191
- ${live ? '<meta http-equiv="refresh" content="10">\n' : ''}<title>${title.replace(/[&<>"]/g, (ch) => `&#${ch.charCodeAt(0)};`)}</title>
1192
- <style>
1193
- body{font:14px/1.5 system-ui,sans-serif;margin:0;color:#24292f}
1194
- main{margin-left:240px;padding:1em 2em;max-width:1600px}
1195
- #toc{position:fixed;top:0;left:0;bottom:0;width:220px;overflow:auto;padding:1em 6px;box-sizing:border-box;border-right:1px solid #d0d7de;background:#f6f8fa;font-size:13px}
1196
- #toc a{display:block;padding:2px 8px;border-radius:4px;color:#24292f;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1197
- #toc a.top{font-weight:600;margin-bottom:6px}
1198
- #toc a.sub{padding-left:22px;color:#57606a}
1199
- #toc a:hover{background:#eaeef2}
1200
- #toc a.on{background:#ddf4ff;color:#0969da;font-weight:600}
1201
- h1,h2,h3{scroll-margin-top:8px}
1202
- @media (max-width:800px){#toc{display:none}main{margin-left:0}}
1203
- table{border-collapse:collapse;margin:1em 0}th,td{border:1px solid #d0d7de;padding:4px 8px;vertical-align:top}
1204
- th{background:#f6f8fa}code{background:#f6f8fa;padding:0 4px;border-radius:4px}
1205
- pre{background:#f6f8fa;padding:8px;overflow:auto}pre.mermaid{background:none}
1206
- a{color:#0969da}
1207
- </style>
1208
- </head>
1209
- <body>
1210
- <nav id="toc"></nav>
1211
- <main id="doc">loading…</main>
1212
- <script id="md" type="application/json">${embed(md)}</script>
1213
- <script type="module">
1214
- const md = JSON.parse(document.getElementById('md').textContent);
1215
- const [{ marked }, { default: DOMPurify }, { default: mermaid }] = await Promise.all([
1216
- import(${embed(MARKED_URL)}),
1217
- import(${embed(PURIFY_URL)}),
1218
- import(${embed(MERMAID_URL)}),
1219
- ]);
1220
- const doc = document.getElementById('doc');
1221
- doc.innerHTML = DOMPurify.sanitize(marked.parse(md));
1222
- // GitHub's slugs, so the diagrams' #anchors land as they do in the .md.
1223
- for (const h of doc.querySelectorAll('h1, h2, h3, h4')) {
1224
- h.id = h.textContent.trim().toLowerCase().replace(/[^\\w\\s-]/g, '').replace(/\\s/g, '-');
1225
- }
1226
- const toc = document.getElementById('toc');
1227
- const heads = [...doc.querySelectorAll('h1, h2, h3')];
1228
- for (const h of heads) {
1229
- const a = document.createElement('a');
1230
- a.href = '#' + h.id;
1231
- a.textContent = h.textContent;
1232
- a.title = h.textContent;
1233
- a.className = h.tagName === 'H1' ? 'top' : h.tagName === 'H3' ? 'sub' : '';
1234
- toc.append(a);
1235
- }
1236
- for (const code of doc.querySelectorAll('code.language-mermaid')) {
1237
- const pre = document.createElement('pre');
1238
- pre.className = 'mermaid';
1239
- pre.textContent = code.textContent;
1240
- code.parentElement.replaceWith(pre);
1241
- }
1242
- mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });
1243
- await mermaid.run({ querySelector: 'pre.mermaid' });
1244
- // A page with an .html twin links to it; any other .md opens as the file it is.
1245
- for (const a of doc.querySelectorAll('a')) {
1246
- for (const at of ['href', 'xlink:href']) {
1247
- const v = a.getAttribute(at);
1248
- if (v) {
1249
- a.setAttribute(at, v.replace(/(^|\\/)(STATUS|FEEDBACK)\\.md(?=#|$)/, '$1$2.html'));
1250
- }
1251
- }
1252
- }
1253
- // The live refresh would otherwise drop the reader back at the top every 10 s.
1254
- const key = 'scroll:' + location.pathname;
1255
- history.scrollRestoration = 'manual';
1256
- addEventListener('beforeunload', () => sessionStorage.setItem(key, String(scrollY)));
1257
- const kept = sessionStorage.getItem(key);
1258
- if (kept !== null) {
1259
- scrollTo(0, Number(kept));
1260
- } else if (location.hash) {
1261
- document.getElementById(decodeURIComponent(location.hash.slice(1)))?.scrollIntoView();
1262
- }
1263
- const mark = () => {
1264
- let current = heads[0];
1265
- for (const h of heads) {
1266
- if (h.getBoundingClientRect().top > 80) break;
1267
- current = h;
1268
- }
1269
- for (const a of toc.children) {
1270
- a.classList.toggle('on', a.hash === '#' + current?.id);
1271
- }
1272
- const on = toc.querySelector('a.on');
1273
- if (on && (on.offsetTop < toc.scrollTop || on.offsetTop + on.offsetHeight > toc.scrollTop + toc.clientHeight)) {
1274
- toc.scrollTop = on.offsetTop - toc.clientHeight / 2;
1275
- }
1276
- };
1277
- addEventListener('scroll', mark, { passive: true });
1278
- mark();
1279
- </script>
1280
- </body>
1281
- </html>
1282
- `;
1394
+ out.push(`- … everything in ${link('events.jsonl', here, join(t.dir, 'events.jsonl'))}`, '', '## Files', '', '- `cases/<id>/FEEDBACK.md` - one case: rubric, latest analysis, decisions, history', '- `cases/<id>/r<rev>/<NN>-<phase>/` - one try: `request.json`, `run.json`, `memory/`, `workspace/`, `brief.md`, `feedback.json`, `analysis.md`, logs', '- `applies/<NNN>/APPLY.md` - one apply: requests, decisions, files changed with their diffs, zen check', '- `applies/<NNN>/` - `inputs.json`, `brief.md`, `apply.log`, `check.log`, `changes.md`, `decisions.json`, `diff.patch`', '- `memories/` - `submissions.jsonl`, `merges.jsonl`, `submitted/<case>@r<rev>/`, `merged/mNN/`', '- `systems/` - `systems.jsonl` and a copy of `agents.yaml` + `agents/` per version', '- `events.jsonl` - every step, in order; `loop.json` - the settings', '- `journal.jsonl` - steps begun and not yet closed; a restart rolls back what a kill cut off', '- `view.json` - where `zen meta finetune start` or `serve` shows these pages live', '');
1395
+ atomic(join(t.dir, 'STATUS.md'), `${out.join('\n')}\n`);
1283
1396
  }
1284
1397
  //# sourceMappingURL=report.js.map