@zenera/cli 1.1.25 → 1.1.26
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.
- package/README.md +13 -1
- package/dist/catalog.d.ts +20 -0
- package/dist/catalog.js +60 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +5 -1
- package/dist/commands/inspect.js +54 -14
- package/dist/commands/memory.js +28 -3
- package/dist/commands/meta.js +24 -5
- package/dist/commands/models.js +2 -1
- package/dist/commands/sandbox.js +73 -3
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +6 -0
- package/dist/eventlog.d.ts +5 -0
- package/dist/eventlog.js +25 -1
- package/dist/liveness.js +20 -2
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +1 -1
- package/dist/meta/editor.js +4 -21
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -0
- package/dist/meta/finetune/command.d.ts +1 -1
- package/dist/meta/finetune/command.js +224 -46
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/types.d.ts +3 -1
- package/dist/meta/finetune/feedback.d.ts +14 -2
- package/dist/meta/finetune/feedback.js +22 -3
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +50 -3
- package/dist/meta/finetune/improvements.js +290 -33
- package/dist/meta/finetune/live.d.ts +17 -7
- package/dist/meta/finetune/live.js +56 -10
- package/dist/meta/finetune/loop.js +59 -10
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +9 -2
- package/dist/meta/finetune/report.js +551 -438
- package/dist/meta/finetune/sampler.js +8 -2
- package/dist/meta/finetune/train.d.ts +6 -2
- package/dist/meta/finetune/train.js +55 -21
- package/dist/meta/finetune/tui.d.ts +3 -3
- package/dist/meta/finetune/tui.js +37 -12
- package/dist/meta/finetune/tuning.d.ts +27 -1
- package/dist/meta/finetune/tuning.js +21 -2
- package/dist/meta/finetune/usage.d.ts +4 -0
- package/dist/meta/finetune/usage.js +35 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +2 -1
- package/dist/meta/host.js +2 -1
- package/dist/meta.d.ts +291 -0
- package/dist/meta.js +981 -0
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/scaffold.d.ts +3 -4
- package/dist/scaffold.js +21 -9
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +54 -5
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +3 -1
- package/dist/tui/models.js +65 -7
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +29 -43
- package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
- package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
- package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -493
- package/templates/project/agents/tools-instructions.md +7 -0
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
379
|
-
|
|
380
|
-
|
|
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(
|
|
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 =
|
|
412
|
-
?
|
|
413
|
-
:
|
|
414
|
-
?
|
|
415
|
-
:
|
|
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.
|
|
533
|
-
?
|
|
534
|
-
:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
639
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
'
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
'
|
|
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
|
-
'
|
|
759
|
-
'
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
`
|
|
764
|
-
'
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
'
|
|
768
|
-
'
|
|
769
|
-
'
|
|
770
|
-
'
|
|
771
|
-
'
|
|
772
|
-
'
|
|
773
|
-
'
|
|
774
|
-
|
|
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
|
|
779
|
-
idle: '
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
921
|
+
const STEP_WORD = {
|
|
922
|
+
idle: 'idle',
|
|
923
|
+
run: 'running',
|
|
924
|
+
analyze: 'grading',
|
|
925
|
+
parked: 'parked',
|
|
783
926
|
};
|
|
784
|
-
//
|
|
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
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
};
|
|
805
|
-
const
|
|
806
|
-
|
|
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
|
-
|
|
816
|
-
'---',
|
|
817
|
-
'config:',
|
|
818
|
-
' flowchart:',
|
|
819
|
-
' wrappingWidth: 480',
|
|
820
|
-
'---',
|
|
960
|
+
...DIAGRAM_HEAD,
|
|
821
961
|
'flowchart TB',
|
|
822
|
-
` DS[("
|
|
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
|
-
|
|
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
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const now = [
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
q.
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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 ? ['
|
|
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
|
-
|
|
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} | ${
|
|
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.
|
|
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 =
|
|
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: ${
|
|
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 ${
|
|
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
|
-
...
|
|
1328
|
+
...s.summary,
|
|
1072
1329
|
'## Overview',
|
|
1073
1330
|
'',
|
|
1074
|
-
...
|
|
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
|
-
|
|
1083
|
-
|
|
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)} |
|
|
1344
|
+
out.push(`| ${w.slot} | - | - | - | - | ${idleWord(t)} | - | - | - | - | - |`);
|
|
1095
1345
|
continue;
|
|
1096
1346
|
}
|
|
1097
|
-
const dir = t.
|
|
1347
|
+
const dir = t.workDir({ ...w, case: w.case });
|
|
1098
1348
|
const run = readJson(join(dir, 'run.json'));
|
|
1099
|
-
const session =
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
1144
|
-
|
|
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', '- `
|
|
1169
|
-
|
|
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
|