@zenera/cli 1.1.24 → 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 +65 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +31 -1
- package/dist/catalog.js +140 -7
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +52 -2
- package/dist/commands/inspect.js +220 -31
- package/dist/commands/memory.js +156 -32
- package/dist/commands/meta.js +333 -24
- package/dist/commands/models.js +97 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +148 -16
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +8 -1
- package/dist/engine.js +7 -0
- package/dist/eventlog.d.ts +64 -0
- package/dist/eventlog.js +170 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +35 -3
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +43 -0
- 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/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +516 -0
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +127 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +57 -0
- package/dist/meta/finetune/feedback.js +104 -0
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +98 -0
- package/dist/meta/finetune/improvements.js +501 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +84 -0
- package/dist/meta/finetune/live.js +272 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +163 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +40 -0
- package/dist/meta/finetune/report.js +1397 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +47 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +43 -0
- package/dist/meta/finetune/train.js +425 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +316 -0
- package/dist/meta/finetune/tuning.d.ts +193 -0
- package/dist/meta/finetune/tuning.js +308 -0
- package/dist/meta/finetune/usage.d.ts +66 -0
- package/dist/meta/finetune/usage.js +260 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +15 -0
- package/dist/meta/host.js +21 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/meta.d.ts +71 -1
- package/dist/meta.js +303 -6
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -7
- package/dist/scaffold.js +24 -67
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +107 -7
- 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 +35 -0
- package/dist/tui/models.js +447 -0
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +4 -2
- package/templates/editor/.github/copilot-instructions.md +60 -145
- package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -494
- package/templates/project/agents/tools-instructions.md +14 -4
- package/templates/project/gitignore +2 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
3
|
+
import { safe } from "../host.js";
|
|
4
|
+
import { DatasetStore } from "./dataset/store.js";
|
|
5
|
+
import { DATASET_DIR } from "./dataset/types.js";
|
|
6
|
+
import { feedbackId, forDataset, lead, readFeedback, } from "./feedback.js";
|
|
7
|
+
import { RECHECK_TRIES } from "./recheck.js";
|
|
8
|
+
import { resultOf, selection } from "./sampler.js";
|
|
9
|
+
import { readJson } from "./tuning.js";
|
|
10
|
+
import { applyTokens, envelopesIn, mergeModels, mergeStages, runMetrics, runTokens, short, STAGES, total, } from "./usage.js";
|
|
11
|
+
const PHASE_WORD = { nomem: 'no memory', mem: 'with memory' };
|
|
12
|
+
function atomic(file, text) {
|
|
13
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
14
|
+
writeFileSync(`${file}.tmp`, text);
|
|
15
|
+
renameSync(`${file}.tmp`, file);
|
|
16
|
+
}
|
|
17
|
+
/** A table cell: one line, no column breaks. */
|
|
18
|
+
export const cell = (text) => String(text ?? '')
|
|
19
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
20
|
+
.replace(/\|/g, '\\|')
|
|
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');
|
|
28
|
+
/** A link from `from` (a directory) to `to`, safe for spaces. */
|
|
29
|
+
const link = (label, from, to) => `[${label}](<${relative(from, to).split(sep).join('/')}>)`;
|
|
30
|
+
export function since(ms) {
|
|
31
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
32
|
+
if (s < 60) {
|
|
33
|
+
return `${s}s`;
|
|
34
|
+
}
|
|
35
|
+
const m = Math.floor(s / 60);
|
|
36
|
+
if (m < 60) {
|
|
37
|
+
return `${m}m ${String(s % 60).padStart(2, '0')}s`;
|
|
38
|
+
}
|
|
39
|
+
return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`;
|
|
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
|
+
}
|
|
50
|
+
export function attemptsOf(t, c) {
|
|
51
|
+
const dir = t.caseDir(c);
|
|
52
|
+
let names = [];
|
|
53
|
+
try {
|
|
54
|
+
names = readdirSync(dir).filter((n) => /^\d{2}-(nomem|mem)$/.test(n));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return names
|
|
60
|
+
.map((name) => {
|
|
61
|
+
const [n, phase] = name.split('-');
|
|
62
|
+
const at = join(dir, name);
|
|
63
|
+
const read = readFeedback(join(at, 'feedback.json'), t.known(c, phase, Number(n)));
|
|
64
|
+
return {
|
|
65
|
+
phase,
|
|
66
|
+
attempt: Number(n),
|
|
67
|
+
dir: at,
|
|
68
|
+
run: readJson(join(at, 'run.json')),
|
|
69
|
+
...('feedback' in read ? { feedback: read.feedback } : {}),
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
.sort((a, b) => Number(a.phase === 'mem') - Number(b.phase === 'mem') || a.attempt - b.attempt);
|
|
73
|
+
}
|
|
74
|
+
function decisions(t) {
|
|
75
|
+
return t.improvements.decisions();
|
|
76
|
+
}
|
|
77
|
+
/** A run's tokens per model: recorded at the run, or read from its trajectory. */
|
|
78
|
+
function runByModel(run) {
|
|
79
|
+
if (!run) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const by = run.tokens ?? runTokens(run.dir);
|
|
83
|
+
if (by && Object.keys(by).length > 0) {
|
|
84
|
+
return by;
|
|
85
|
+
}
|
|
86
|
+
return run.usage
|
|
87
|
+
? {
|
|
88
|
+
'?': {
|
|
89
|
+
calls: run.turns ?? 0,
|
|
90
|
+
input: run.usage.inputTokens,
|
|
91
|
+
cached: run.usage.cachedInputTokens ?? 0,
|
|
92
|
+
output: run.usage.outputTokens,
|
|
93
|
+
reasoning: run.usage.reasoningTokens ?? 0,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
/** Every try of a case, per stage and model. Applies serve many cases at once, so they are not split. */
|
|
99
|
+
export function caseTokens(t, c) {
|
|
100
|
+
const attempts = attemptsOf(t, c);
|
|
101
|
+
return mergeStages(...attempts.map((a) => ({
|
|
102
|
+
run: runByModel(a.run),
|
|
103
|
+
analyze: envelopesIn(a.dir, 'analyze'),
|
|
104
|
+
})));
|
|
105
|
+
}
|
|
106
|
+
/** `in / out`, the input counting cached tokens. */
|
|
107
|
+
const inOut = (by) => {
|
|
108
|
+
const s = total(by);
|
|
109
|
+
return s.input + s.output > 0 ? `${short(s.input)} / ${short(s.output)}` : '-';
|
|
110
|
+
};
|
|
111
|
+
function tokenTable(by) {
|
|
112
|
+
const rows = [];
|
|
113
|
+
for (const stage of STAGES) {
|
|
114
|
+
const models = Object.entries(by[stage] ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
115
|
+
for (const [model, s] of models) {
|
|
116
|
+
rows.push(`| ${stage} | ${cell(model)} | ${s.calls} | ${short(s.input)} | ${short(s.cached)} | ${short(s.output)} | ${short(s.reasoning)} |`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (rows.length === 0) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
return [
|
|
123
|
+
'| Stage | Model | Calls | Input | Cached | Output | Reasoning |',
|
|
124
|
+
'| --- | --- | --- | --- | --- | --- | --- |',
|
|
125
|
+
...rows,
|
|
126
|
+
'',
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
export const rubricScore = (f) => {
|
|
130
|
+
const r = Object.values(f?.rubric ?? {});
|
|
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)` : ''}`;
|
|
137
|
+
};
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// What a run did, and how it moved from the try it is compared with
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
const FLAT_KEYS = [
|
|
142
|
+
'calls',
|
|
143
|
+
'input',
|
|
144
|
+
'cached',
|
|
145
|
+
'output',
|
|
146
|
+
'reasoning',
|
|
147
|
+
'peak',
|
|
148
|
+
'llmMs',
|
|
149
|
+
'wallMs',
|
|
150
|
+
'toolMs',
|
|
151
|
+
'tools',
|
|
152
|
+
'toolErrors',
|
|
153
|
+
'recall',
|
|
154
|
+
'load',
|
|
155
|
+
'grep',
|
|
156
|
+
'commit',
|
|
157
|
+
'forget',
|
|
158
|
+
'forks',
|
|
159
|
+
'branches',
|
|
160
|
+
'compactions',
|
|
161
|
+
'handoffs',
|
|
162
|
+
];
|
|
163
|
+
const blank = () => Object.fromEntries(FLAT_KEYS.map((k) => [k, 0]));
|
|
164
|
+
function addFlat(...all) {
|
|
165
|
+
const out = blank();
|
|
166
|
+
for (const f of all) {
|
|
167
|
+
for (const k of FLAT_KEYS) {
|
|
168
|
+
out[k] += f?.[k] ?? 0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/** Recorded at the run, or read from its trajectory when the run predates that. */
|
|
174
|
+
const metricsOf = (run) => run ? (run.metrics ?? runMetrics(run.dir)) : undefined;
|
|
175
|
+
const modelFlat = (x) => ({
|
|
176
|
+
...blank(),
|
|
177
|
+
...(x
|
|
178
|
+
? {
|
|
179
|
+
calls: x.calls,
|
|
180
|
+
input: x.input,
|
|
181
|
+
cached: x.cached,
|
|
182
|
+
output: x.output,
|
|
183
|
+
reasoning: x.reasoning,
|
|
184
|
+
peak: x.peak,
|
|
185
|
+
llmMs: x.ms,
|
|
186
|
+
}
|
|
187
|
+
: {}),
|
|
188
|
+
});
|
|
189
|
+
function runFlat(run) {
|
|
190
|
+
const m = metricsOf(run);
|
|
191
|
+
if (!run || !m) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const models = Object.values(m.models);
|
|
195
|
+
return {
|
|
196
|
+
...addFlat(...models.map(modelFlat)),
|
|
197
|
+
peak: Math.max(0, ...models.map((x) => x.peak)),
|
|
198
|
+
llmMs: m.llmMs,
|
|
199
|
+
wallMs: run.durationMs ?? 0,
|
|
200
|
+
toolMs: m.toolMs,
|
|
201
|
+
tools: m.toolCalls,
|
|
202
|
+
toolErrors: m.toolErrors,
|
|
203
|
+
...m.memory,
|
|
204
|
+
forks: m.forks,
|
|
205
|
+
branches: m.branches,
|
|
206
|
+
compactions: m.compactions,
|
|
207
|
+
handoffs: m.handoffs,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const tokensIn = (f) => f.input + f.output;
|
|
211
|
+
const MODEL_METRICS = [
|
|
212
|
+
{ label: 'LLM calls', kind: 'count', better: 'lower', of: (f) => f.calls },
|
|
213
|
+
{ label: 'Input', kind: 'tokens', better: 'lower', of: (f) => f.input },
|
|
214
|
+
{ label: 'Cached', kind: 'tokens', of: (f) => f.cached },
|
|
215
|
+
{ label: 'Uncached input', kind: 'tokens', better: 'lower', of: (f) => f.input - f.cached },
|
|
216
|
+
{
|
|
217
|
+
label: 'Cache hit',
|
|
218
|
+
kind: 'share',
|
|
219
|
+
better: 'higher',
|
|
220
|
+
of: (f) => (f.input ? (f.cached / f.input) * 100 : 0),
|
|
221
|
+
},
|
|
222
|
+
{ label: 'Output', kind: 'tokens', better: 'lower', of: (f) => f.output },
|
|
223
|
+
{ label: 'Reasoning', kind: 'tokens', better: 'lower', of: (f) => f.reasoning },
|
|
224
|
+
{ label: 'Peak input', kind: 'tokens', better: 'lower', of: (f) => f.peak },
|
|
225
|
+
{ label: 'LLM time', kind: 'time', better: 'lower', of: (f) => f.llmMs },
|
|
226
|
+
];
|
|
227
|
+
const RUN_METRICS = [
|
|
228
|
+
{ label: 'Wall clock', kind: 'time', better: 'lower', of: (f) => f.wallMs },
|
|
229
|
+
...MODEL_METRICS,
|
|
230
|
+
{ label: 'Tool calls', kind: 'count', better: 'lower', of: (f) => f.tools },
|
|
231
|
+
{ label: 'Tool errors', kind: 'count', better: 'lower', of: (f) => f.toolErrors },
|
|
232
|
+
{ label: 'Tool time', kind: 'time', better: 'lower', of: (f) => f.toolMs },
|
|
233
|
+
{ label: 'Memory recalls', kind: 'count', of: (f) => f.recall },
|
|
234
|
+
{ label: 'Memory loads', kind: 'count', of: (f) => f.load },
|
|
235
|
+
{ label: 'Memory greps', kind: 'count', of: (f) => f.grep },
|
|
236
|
+
{ label: 'Memory commits', kind: 'count', of: (f) => f.commit },
|
|
237
|
+
{ label: 'Memory forgets', kind: 'count', of: (f) => f.forget },
|
|
238
|
+
{ label: 'Forks', kind: 'count', of: (f) => f.forks },
|
|
239
|
+
{ label: 'Branches', kind: 'count', of: (f) => f.branches },
|
|
240
|
+
{ label: 'Compactions', kind: 'count', of: (f) => f.compactions },
|
|
241
|
+
{ label: 'Handoffs', kind: 'count', of: (f) => f.handoffs },
|
|
242
|
+
];
|
|
243
|
+
const SUMMARY_METRICS = RUN_METRICS.filter((m) => [
|
|
244
|
+
'Wall clock',
|
|
245
|
+
'LLM calls',
|
|
246
|
+
'Tool calls',
|
|
247
|
+
'Tool errors',
|
|
248
|
+
'Input',
|
|
249
|
+
'Uncached input',
|
|
250
|
+
'Cache hit',
|
|
251
|
+
'Output',
|
|
252
|
+
'LLM time',
|
|
253
|
+
'Tool time',
|
|
254
|
+
].includes(m.label));
|
|
255
|
+
function show(kind, n) {
|
|
256
|
+
switch (kind) {
|
|
257
|
+
case 'tokens':
|
|
258
|
+
return short(n);
|
|
259
|
+
case 'time':
|
|
260
|
+
return since(n);
|
|
261
|
+
case 'share':
|
|
262
|
+
return `${Math.round(n)}%`;
|
|
263
|
+
default:
|
|
264
|
+
return String(n);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Percent for amounts, points for a share, a plain difference for a count unless `relative`. */
|
|
268
|
+
function change(kind, now, before, relative = kind !== 'count') {
|
|
269
|
+
const signed = (d, unit) => d === 0 ? '=' : `${d > 0 ? '+' : ''}${d}${unit}`;
|
|
270
|
+
if (kind === 'share') {
|
|
271
|
+
return signed(Math.round(now - before), 'pt');
|
|
272
|
+
}
|
|
273
|
+
if (!relative) {
|
|
274
|
+
return signed(now - before, '');
|
|
275
|
+
}
|
|
276
|
+
if (before === 0) {
|
|
277
|
+
return now === 0 ? '=' : 'new';
|
|
278
|
+
}
|
|
279
|
+
return signed(Math.round(((now - before) / before) * 100), '%');
|
|
280
|
+
}
|
|
281
|
+
/** Within this many percent (points, for a share) is noise, not a trend. */
|
|
282
|
+
const SAME = 5;
|
|
283
|
+
function trend(m, now, before) {
|
|
284
|
+
if (!m.better) {
|
|
285
|
+
return '';
|
|
286
|
+
}
|
|
287
|
+
const d = m.kind === 'share'
|
|
288
|
+
? now - before
|
|
289
|
+
: before === 0
|
|
290
|
+
? Math.sign(now) * Infinity
|
|
291
|
+
: ((now - before) / before) * 100;
|
|
292
|
+
if (Number.isNaN(d) || Math.abs(d) < SAME) {
|
|
293
|
+
return 'same';
|
|
294
|
+
}
|
|
295
|
+
return d < 0 === (m.better === 'lower') ? 'better' : 'worse';
|
|
296
|
+
}
|
|
297
|
+
/** Rows are metrics, columns tries; a metric that is zero everywhere is left out. */
|
|
298
|
+
function matrix(metrics, cols, effect) {
|
|
299
|
+
const all = [...cols.map((c) => c.now), effect?.now, effect?.base];
|
|
300
|
+
const shown = metrics.filter((m) => all.some((f) => f && m.of(f) !== 0));
|
|
301
|
+
if (shown.length === 0) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
const both = effect?.now && effect.base ? { now: effect.now, base: effect.base } : undefined;
|
|
305
|
+
const head = ['Metric', ...cols.map((c) => c.label), ...(both ? ['Memory effect'] : [])];
|
|
306
|
+
const rows = shown.map((m) => {
|
|
307
|
+
const cells = cols.map((c) => {
|
|
308
|
+
if (!c.now) {
|
|
309
|
+
return '-';
|
|
310
|
+
}
|
|
311
|
+
const v = show(m.kind, m.of(c.now));
|
|
312
|
+
return c.base ? `${v} (${change(m.kind, m.of(c.now), m.of(c.base))})` : v;
|
|
313
|
+
});
|
|
314
|
+
if (both) {
|
|
315
|
+
const now = m.of(both.now);
|
|
316
|
+
const before = m.of(both.base);
|
|
317
|
+
const word = trend(m, now, before);
|
|
318
|
+
cells.push(`${change(m.kind, now, before, true)}${word ? ` ${word}` : ''}`);
|
|
319
|
+
}
|
|
320
|
+
return `| ${m.label} | ${cells.join(' | ')} |`;
|
|
321
|
+
});
|
|
322
|
+
return [`| ${head.join(' | ')} |`, `| ${head.map(() => '---').join(' | ')} |`, ...rows, ''];
|
|
323
|
+
}
|
|
324
|
+
/** The no-memory try that passed: the one every try with memory is measured against. */
|
|
325
|
+
const passedWithout = (attempts) => attempts.find((a) => a.phase === 'nomem' && a.feedback?.done);
|
|
326
|
+
const latestWith = (attempts) => [...attempts].reverse().find((a) => a.phase === 'mem' && a.run);
|
|
327
|
+
/** Each try against the one before it in its phase; the first with memory against the passing one without. */
|
|
328
|
+
function tryColumns(attempts, flat) {
|
|
329
|
+
const passed = passedWithout(attempts);
|
|
330
|
+
const cols = attempts.map((a, i) => {
|
|
331
|
+
const base = attempts
|
|
332
|
+
.slice(0, i)
|
|
333
|
+
.reverse()
|
|
334
|
+
.find((b) => b.phase === a.phase) ?? (a.phase === 'mem' ? passed : undefined);
|
|
335
|
+
return {
|
|
336
|
+
label: `${PHASE_WORD[a.phase]}, try ${a.attempt}`,
|
|
337
|
+
now: flat(a),
|
|
338
|
+
base: base && flat(base),
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
const mem = latestWith(attempts);
|
|
342
|
+
return {
|
|
343
|
+
cols,
|
|
344
|
+
...(passed && mem ? { effect: { now: flat(mem), base: flat(passed) } } : {}),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** Before and after, one row per metric, with whether it moved the right way. */
|
|
348
|
+
function compareTable(before, after, a, b) {
|
|
349
|
+
const rows = SUMMARY_METRICS.filter((m) => m.of(a) !== 0 || m.of(b) !== 0).map((m) => `| ${m.label} | ${show(m.kind, m.of(a))} | ${show(m.kind, m.of(b))} | ${change(m.kind, m.of(b), m.of(a), true)} | ${trend(m, m.of(b), m.of(a)) || '-'} |`);
|
|
350
|
+
return [
|
|
351
|
+
`| Metric | ${before} | ${after} | Change | Trend |`,
|
|
352
|
+
'| --- | --- | --- | --- | --- |',
|
|
353
|
+
...rows,
|
|
354
|
+
'',
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
function stateOf(t, c) {
|
|
358
|
+
const r = resultOf(t, c);
|
|
359
|
+
if (r) {
|
|
360
|
+
if (r.state === 'failed') {
|
|
361
|
+
return `failed (${r.reason ?? r.phase}) - resumes at the next start`;
|
|
362
|
+
}
|
|
363
|
+
return r.state === 'completed' ? 'completed' : `difficult (${r.reason ?? r.phase})`;
|
|
364
|
+
}
|
|
365
|
+
const w = t.workers.find((x) => x.case?.id === c.id);
|
|
366
|
+
if (w) {
|
|
367
|
+
return `${PHASE_WORD[w.phase ?? 'nomem']} · try ${w.attempt} · ${w.step}`;
|
|
368
|
+
}
|
|
369
|
+
const parked = t.improvements.pending.find((f) => f.case === c.id);
|
|
370
|
+
if (parked) {
|
|
371
|
+
return `${PHASE_WORD[parked.phase]} · try ${parked.attempt} · parked`;
|
|
372
|
+
}
|
|
373
|
+
const applying = t.improvements.inApply.find((f) => f.case === c.id);
|
|
374
|
+
if (applying) {
|
|
375
|
+
return `${PHASE_WORD[applying.phase]} · try ${applying.attempt} · parked, being applied`;
|
|
376
|
+
}
|
|
377
|
+
if (t.claimed.has(c.id)) {
|
|
378
|
+
return 'waiting for a worker';
|
|
379
|
+
}
|
|
380
|
+
return attemptsOf(t, c).length > 0 ? 'stopped' : 'queued';
|
|
381
|
+
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// FEEDBACK.md - one case
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
export function feedbackPath(t, c) {
|
|
386
|
+
return join(t.dir, 'cases', c.id, 'FEEDBACK.md');
|
|
387
|
+
}
|
|
388
|
+
export function writeFeedback(t, c) {
|
|
389
|
+
const file = feedbackPath(t, c);
|
|
390
|
+
const here = dirname(file);
|
|
391
|
+
const attempts = attemptsOf(t, c);
|
|
392
|
+
const latest = [...attempts].reverse().find((a) => a.feedback);
|
|
393
|
+
const decided = decisions(t);
|
|
394
|
+
const session = [...attempts]
|
|
395
|
+
.reverse()
|
|
396
|
+
.map((a) => analyzeSession(a.dir))
|
|
397
|
+
.find(Boolean);
|
|
398
|
+
const lines = [];
|
|
399
|
+
const say = (...more) => {
|
|
400
|
+
lines.push(...more);
|
|
401
|
+
};
|
|
402
|
+
say(`# ${c.id}`, '', `${link('← status', here, join(t.dir, 'STATUS.md'))}`, '');
|
|
403
|
+
say('| Class | Complexity | Dataset rev | State | System | Session |', '| --- | --- | --- | --- | --- | --- |', `| ${cell(c.class)} | ${cell(c.complexity)} | ${c.rev} | ${cell(stateOf(t, c))} | v${t.system.version()} | ${session ? `\`zen meta resume ${basename(t.root)} ${session}\`` : '-'} |`, '');
|
|
404
|
+
if (c.rubric.length > 0) {
|
|
405
|
+
say('## Rubric', '', ...c.rubric.map((r) => `- **${r.id}** ${r.text}`), '');
|
|
406
|
+
}
|
|
407
|
+
const input = typeof c.input === 'string'
|
|
408
|
+
? c.input
|
|
409
|
+
: c.input
|
|
410
|
+
.map((p) => typeof p === 'string' ? p : 'text' in p ? p.text : `[${Object.keys(p)[0]}]`)
|
|
411
|
+
.join(' ');
|
|
412
|
+
say('## Input', '', ...input
|
|
413
|
+
.slice(0, 800)
|
|
414
|
+
.split('\n')
|
|
415
|
+
.map((l) => `> ${l}`), '');
|
|
416
|
+
if (latest?.feedback) {
|
|
417
|
+
const f = latest.feedback;
|
|
418
|
+
say(`## Latest analysis - ${PHASE_WORD[latest.phase]}, try ${latest.attempt}: ${f.verdict ?? '?'}, rubric ${rubricScore(f)}${f.done ? ', done' : ''}`, '');
|
|
419
|
+
if (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(', ')}.`, '');
|
|
424
|
+
}
|
|
425
|
+
const id = feedbackId(c.id, latest.phase, latest.attempt, t.round(c));
|
|
426
|
+
const improvements = f.improvements ?? [];
|
|
427
|
+
if (improvements.length > 0) {
|
|
428
|
+
say('### Improvements', '', '| # | Change | File | Decision |', '| --- | --- | --- | --- |');
|
|
429
|
+
for (const i of improvements) {
|
|
430
|
+
const d = decided[id]?.[i.id];
|
|
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';
|
|
438
|
+
say(`| ${cell(i.id)} | ${cell(i.change)} | ${cell(i.file)} | ${cell(decision)} |`);
|
|
439
|
+
}
|
|
440
|
+
say('');
|
|
441
|
+
}
|
|
442
|
+
const analysis = join(latest.dir, 'analysis.md');
|
|
443
|
+
if (existsSync(analysis)) {
|
|
444
|
+
say('### The analysis', '', readFileSync(analysis, 'utf8').trim(), '');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (attempts.length > 0) {
|
|
448
|
+
say('## History', '', '| Phase | Try | Verdict | Rubric | LLM calls | Tool calls | Run tokens | Analyze tokens | Seconds | System | Links |', '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
449
|
+
for (const a of [...attempts].reverse()) {
|
|
450
|
+
const links = [
|
|
451
|
+
a.run ? link('run', here, a.run.dir) : '',
|
|
452
|
+
a.run?.report ? link('report', here, a.run.report) : '',
|
|
453
|
+
existsSync(join(a.dir, 'analysis.md'))
|
|
454
|
+
? link('analysis', here, join(a.dir, 'analysis.md'))
|
|
455
|
+
: '',
|
|
456
|
+
existsSync(join(a.dir, 'feedback.json'))
|
|
457
|
+
? link('feedback', here, join(a.dir, 'feedback.json'))
|
|
458
|
+
: '',
|
|
459
|
+
].filter(Boolean);
|
|
460
|
+
say(`| ${PHASE_WORD[a.phase]} | ${a.attempt} | ${cell(a.feedback?.verdict ?? (a.run ? 'analyzing' : 'running'))} | ${rubricScore(a.feedback)} | ${cell(a.run?.turns)} | ${cell(metricsOf(a.run)?.toolCalls)} | ${inOut(runByModel(a.run))} | ${inOut(envelopesIn(a.dir, 'analyze'))} | ${a.run?.durationMs ? Math.round(a.run.durationMs / 1000) : '-'} | ${a.run ? `v${a.run.system}` : '-'} | ${links.join(' · ') || '-'} |`);
|
|
461
|
+
}
|
|
462
|
+
say('');
|
|
463
|
+
}
|
|
464
|
+
const byRun = tryColumns(attempts, (a) => runFlat(a.run));
|
|
465
|
+
const runRows = matrix(RUN_METRICS, byRun.cols, byRun.effect);
|
|
466
|
+
if (runRows.length > 0) {
|
|
467
|
+
say('## Metrics', '', 'Each try against the one before it in its phase, the first try with memory against the passing try without; Memory effect is the latest try with memory against that same passing try. LLM and tool time are summed over parallel branches.', '', ...runRows);
|
|
468
|
+
const models = [
|
|
469
|
+
...new Set(attempts.flatMap((a) => Object.keys(metricsOf(a.run)?.models ?? {}))),
|
|
470
|
+
].sort();
|
|
471
|
+
if (models.length > 1) {
|
|
472
|
+
for (const model of models) {
|
|
473
|
+
const per = tryColumns(attempts, (a) => {
|
|
474
|
+
const m = metricsOf(a.run);
|
|
475
|
+
return m && modelFlat(m.models[model]);
|
|
476
|
+
});
|
|
477
|
+
say(`### ${cell(model)}`, '', ...matrix(MODEL_METRICS, per.cols, per.effect));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const byStage = caseTokens(t, c);
|
|
482
|
+
if (Object.keys(byStage).length > 0) {
|
|
483
|
+
say('## Tokens', '', ...tokenTable(byStage));
|
|
484
|
+
}
|
|
485
|
+
const submitted = t.memories.submissions().find((s) => s.key === `${c.id}@r${c.rev}`);
|
|
486
|
+
if (submitted) {
|
|
487
|
+
const merges = t.memories
|
|
488
|
+
.merges()
|
|
489
|
+
.filter((m) => m.ok && m.includes.includes(submitted.key));
|
|
490
|
+
say('## Memory', '', submitted.empty
|
|
491
|
+
? `The passing run with memory (try ${submitted.attempt}) committed nothing, so there was nothing to keep.`
|
|
492
|
+
: `Kept ${submitted.at.slice(0, 16).replace('T', ' ')} from the passing run with memory (try ${submitted.attempt}): ${link('the copy', here, submitted.dir)}.`, '');
|
|
493
|
+
if (!submitted.empty) {
|
|
494
|
+
say(merges.length > 0
|
|
495
|
+
? `In merges: ${merges.map((m) => link(m.name, here, m.dir)).join(', ')}.`
|
|
496
|
+
: 'Not merged yet.', '');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const md = `${lines.join('\n')}\n`;
|
|
500
|
+
atomic(file, md);
|
|
501
|
+
}
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// APPLY.md - one apply: every request, every decision, every file it changed
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
/** A fence longer than any run of backticks in `text`, so nothing inside can close it. */
|
|
506
|
+
export function fence(text) {
|
|
507
|
+
const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((r) => r.length));
|
|
508
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
509
|
+
}
|
|
510
|
+
export function patchFiles(patch) {
|
|
511
|
+
return patch
|
|
512
|
+
.split(/^(?=diff --git )/m)
|
|
513
|
+
.filter((p) => p.startsWith('diff --git '))
|
|
514
|
+
.map((text) => {
|
|
515
|
+
const m = /^diff --git a\/(\S+) b\/(\S+)/.exec(text);
|
|
516
|
+
const file = (m?.[2] ?? '?').replace(/^v\d+\//, '');
|
|
517
|
+
let added = 0;
|
|
518
|
+
let removed = 0;
|
|
519
|
+
for (const line of text.split('\n')) {
|
|
520
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (line.startsWith('+')) {
|
|
524
|
+
added++;
|
|
525
|
+
}
|
|
526
|
+
else if (line.startsWith('-')) {
|
|
527
|
+
removed++;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return { file, added, removed, text: text.trimEnd() };
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
export function applyPath(dir) {
|
|
534
|
+
return join(dir, 'APPLY.md');
|
|
535
|
+
}
|
|
536
|
+
export function writeApply(t, dir) {
|
|
537
|
+
const name = basename(dir);
|
|
538
|
+
const here = dir;
|
|
539
|
+
const inputs = readJson(join(dir, 'inputs.json')) ?? [];
|
|
540
|
+
const applied = readJson(join(dir, 'applied.json'));
|
|
541
|
+
const failed = readJson(join(dir, 'failed.json'));
|
|
542
|
+
const decided = readJson(join(dir, 'decisions.json')) ?? {};
|
|
543
|
+
const envelope = readJson(join(dir, 'apply.json'));
|
|
544
|
+
const retried = existsSync(join(dir, 'apply-2.json'));
|
|
545
|
+
const patch = existsSync(join(dir, 'diff.patch'))
|
|
546
|
+
? readFileSync(join(dir, 'diff.patch'), 'utf8')
|
|
547
|
+
: '';
|
|
548
|
+
const files = patchFiles(patch);
|
|
549
|
+
const improvements = inputs.reduce((n, f) => n + f.improvements.length, 0);
|
|
550
|
+
const consolidation = applied?.kind === 'consolidate' || existsSync(join(dir, 'consolidate.json'));
|
|
551
|
+
const status = failed
|
|
552
|
+
? 'failed - the project was put back as it was'
|
|
553
|
+
: applied
|
|
554
|
+
? applied.discarded
|
|
555
|
+
? `undone - ${applied.discarded}`
|
|
556
|
+
: applied.skipped
|
|
557
|
+
? 'nothing to change'
|
|
558
|
+
: 'applied'
|
|
559
|
+
: 'in progress';
|
|
560
|
+
const started = applied?.startedAt ??
|
|
561
|
+
failed?.startedAt ??
|
|
562
|
+
(existsSync(join(dir, 'inputs.json'))
|
|
563
|
+
? statSync(join(dir, 'inputs.json')).mtime.toISOString()
|
|
564
|
+
: undefined);
|
|
565
|
+
const finished = applied?.at ?? failed?.at;
|
|
566
|
+
const when = (iso) => iso ? iso.slice(0, 19).replace('T', ' ') : '-';
|
|
567
|
+
const took = started && finished ? since(Date.parse(finished) - Date.parse(started)) : '-';
|
|
568
|
+
const tally = {};
|
|
569
|
+
for (const f of inputs) {
|
|
570
|
+
for (const i of f.improvements) {
|
|
571
|
+
const d = decided[f.id]?.[i.id]?.decision ?? 'undecided';
|
|
572
|
+
tally[d] = (tally[d] ?? 0) + 1;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const out = [
|
|
576
|
+
`# ${consolidation ? 'Consolidation' : 'Apply'} ${name}${applied && !applied.discarded ? ` - v${applied.from} → v${applied.to}` : ''}`,
|
|
577
|
+
'',
|
|
578
|
+
link('← status', here, join(t.dir, 'STATUS.md')),
|
|
579
|
+
'',
|
|
580
|
+
'| Status | Started | Finished | Took | Requests | Improvements | Files changed | Retried |',
|
|
581
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
582
|
+
`| ${cell(status)} | ${when(started)} | ${when(finished)} | ${took} | ${inputs.length} | ${improvements} | ${files.length} | ${retried ? 'yes - the first attempt was undone' : 'no'} |`,
|
|
583
|
+
'',
|
|
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
|
+
}
|
|
588
|
+
if (failed) {
|
|
589
|
+
out.push(`> ${cell(failed.error)}`, '');
|
|
590
|
+
}
|
|
591
|
+
if (envelope?.sessionId) {
|
|
592
|
+
out.push(`The apply session: \`zen meta resume ${basename(t.root)} ${envelope.sessionId}\` - ask it why it changed what it changed.`, '');
|
|
593
|
+
}
|
|
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
|
+
}
|
|
602
|
+
inputs.forEach((f, n) => {
|
|
603
|
+
const tryDir = f.file
|
|
604
|
+
? dirname(f.file)
|
|
605
|
+
: t.attemptDir({ id: f.case, rev: f.caseRev }, f.phase, f.attempt);
|
|
606
|
+
const run = readJson(join(tryDir, 'run.json'));
|
|
607
|
+
const links = [
|
|
608
|
+
link('case', here, feedbackPath(t, { id: f.case })),
|
|
609
|
+
existsSync(join(tryDir, 'analysis.md'))
|
|
610
|
+
? link('analysis', here, join(tryDir, 'analysis.md'))
|
|
611
|
+
: '',
|
|
612
|
+
existsSync(join(tryDir, 'feedback.json'))
|
|
613
|
+
? link('feedback', here, join(tryDir, 'feedback.json'))
|
|
614
|
+
: '',
|
|
615
|
+
run ? link('run', here, run.dir) : '',
|
|
616
|
+
run?.report ? link('report', here, run.report) : '',
|
|
617
|
+
].filter(Boolean);
|
|
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(' · ')}`, '');
|
|
619
|
+
if (f.summary) {
|
|
620
|
+
out.push(quote(f.summary), '');
|
|
621
|
+
}
|
|
622
|
+
if (f.improvements.length === 0) {
|
|
623
|
+
out.push('No improvements asked for.', '');
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
out.push('| # | Kind | File | Change | Why | Expect | Decision | Note |', '| --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
627
|
+
for (const i of f.improvements) {
|
|
628
|
+
const d = decided[f.id]?.[i.id];
|
|
629
|
+
out.push(`| ${cell(i.id)} | ${cell(i.kind)} | ${cell(i.file)} | ${cell(i.change)} | ${cell(i.why)} | ${cell(i.expect)} | ${cell(d?.decision ?? (applied || failed ? 'undecided' : 'pending'))} | ${cell(d?.note)} |`);
|
|
630
|
+
}
|
|
631
|
+
out.push('');
|
|
632
|
+
});
|
|
633
|
+
out.push('## What the apply agent did', '');
|
|
634
|
+
const changes = join(dir, 'changes.md');
|
|
635
|
+
out.push(existsSync(changes)
|
|
636
|
+
? readFileSync(changes, 'utf8').trim().replace(/^#/gm, '###')
|
|
637
|
+
: '_The apply agent wrote no changes.md._', '');
|
|
638
|
+
if (envelope?.answer) {
|
|
639
|
+
out.push('### Its answer', '', ...envelope.answer
|
|
640
|
+
.trim()
|
|
641
|
+
.split('\n')
|
|
642
|
+
.map((l) => `> ${l}`), '');
|
|
643
|
+
}
|
|
644
|
+
out.push('## Files changed', '');
|
|
645
|
+
if (files.length === 0) {
|
|
646
|
+
out.push(applied ? '_None: the system is unchanged._' : '_Not yet._', '');
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
out.push('| File | Added | Removed |', '| --- | --- | --- |', ...files.map((f) => `| \`${f.file}\` | +${f.added} | -${f.removed} |`), '');
|
|
650
|
+
for (const f of files) {
|
|
651
|
+
const tick = fence(f.text);
|
|
652
|
+
out.push(`### \`${f.file}\``, '', `${tick}diff`, f.text, tick, '');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
out.push('## zen check', '');
|
|
656
|
+
const check = join(dir, 'check.log');
|
|
657
|
+
if (existsSync(check)) {
|
|
658
|
+
const tail = readFileSync(check, 'utf8').trimEnd().split('\n').slice(-40).join('\n');
|
|
659
|
+
const verdict = failed ? 'failed on both attempts' : applied ? 'passed' : 'running';
|
|
660
|
+
const tick = fence(tail);
|
|
661
|
+
out.push(verdict, '', `${tick}text`, tail || '(no output)', tick, '');
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
out.push(applied?.skipped ? '_Not run: nothing was changed._' : '_Not run yet._', '');
|
|
665
|
+
}
|
|
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}`), '');
|
|
671
|
+
}
|
|
672
|
+
out.push(...recheckSection(t, here, (r) => r.apply === name));
|
|
673
|
+
atomic(applyPath(dir), `${out.join('\n')}\n`);
|
|
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
|
+
}
|
|
731
|
+
// ---------------------------------------------------------------------------
|
|
732
|
+
// The memory section of STATUS.md
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
function memorySection(t, here) {
|
|
735
|
+
const m = t.memories;
|
|
736
|
+
const submissions = m.submissions();
|
|
737
|
+
const merges = m.merges();
|
|
738
|
+
if (submissions.length === 0 && merges.length === 0) {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
const caseLink = (key) => {
|
|
742
|
+
const id = key.replace(/@r\d+$/, '');
|
|
743
|
+
return link(id, here, feedbackPath(t, { id }));
|
|
744
|
+
};
|
|
745
|
+
const waiting = m.unmerged();
|
|
746
|
+
const out = [
|
|
747
|
+
'## Memory',
|
|
748
|
+
'',
|
|
749
|
+
'No run shares a memory: every try runs on its own private directory. A case whose',
|
|
750
|
+
`with-memory run passes keeps a copy of that memory; every ${t.config.mergeEvery} kept memories are`,
|
|
751
|
+
'merged, with the last merge, into a new numbered graph.',
|
|
752
|
+
'',
|
|
753
|
+
`kept **${submissions.length}** (${submissions.filter((s) => s.empty).length} committed nothing) · merges **${merges.filter((x) => x.ok).length}** · waiting to merge **${waiting.length} of ${t.config.mergeEvery}**${waiting.length ? `: ${waiting.map((s) => caseLink(s.key)).join(', ')}` : ''}`,
|
|
754
|
+
'',
|
|
755
|
+
];
|
|
756
|
+
if (merges.length > 0) {
|
|
757
|
+
out.push('| Merge | At | Cases in it | New in this merge | Nodes | Edges | Graph | Log |', '| --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
758
|
+
for (const x of [...merges].reverse()) {
|
|
759
|
+
out.push(`| ${x.name}${x.ok ? '' : ' (failed)'} | ${x.at.slice(11, 16)} | ${x.includes.length}: ${x.includes.map(caseLink).join(', ')} | ${x.added.map(caseLink).join(', ')} | ${x.nodes ?? '-'} | ${x.edges ?? '-'} | ${x.ok ? link('graph', here, x.dir) : '-'} | ${link('log', here, x.log)} |`);
|
|
760
|
+
}
|
|
761
|
+
out.push('');
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
// STATUS.md - the whole tuning
|
|
767
|
+
// ---------------------------------------------------------------------------
|
|
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;
|
|
795
|
+
function idleWord(t) {
|
|
796
|
+
if (t.improvements.applying) {
|
|
797
|
+
return 'idle - no run starts during an apply';
|
|
798
|
+
}
|
|
799
|
+
return t.drained ? 'idle - no case left to start' : 'idle';
|
|
800
|
+
}
|
|
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) {
|
|
848
|
+
const q = t.improvements;
|
|
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
|
+
};
|
|
856
|
+
};
|
|
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');
|
|
875
|
+
}
|
|
876
|
+
return [
|
|
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',
|
|
918
|
+
'```',
|
|
919
|
+
];
|
|
920
|
+
}
|
|
921
|
+
const STEP_WORD = {
|
|
922
|
+
idle: 'idle',
|
|
923
|
+
run: 'running',
|
|
924
|
+
analyze: 'grading',
|
|
925
|
+
parked: 'parked',
|
|
926
|
+
};
|
|
927
|
+
// Edges end on the box, so it keeps LR; the invisible chain keeps the slots in order.
|
|
928
|
+
const inOrder = (prefix, n) => n > 1
|
|
929
|
+
? [` ${Array.from({ length: n }, (_, i) => `${prefix}${i + 1}`).join(' ~~~ ')}`]
|
|
930
|
+
: [];
|
|
931
|
+
// A markdown label: real newlines, since some previews swallow `<br/>` before Mermaid sees it.
|
|
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) {
|
|
943
|
+
const q = t.improvements;
|
|
944
|
+
const m = t.memories;
|
|
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);
|
|
957
|
+
};
|
|
958
|
+
const { clicks, click } = clickable(t);
|
|
959
|
+
const out = [
|
|
960
|
+
...DIAGRAM_HEAD,
|
|
961
|
+
'flowchart TB',
|
|
962
|
+
` DS[("${lines('**cases**', `${counts.queued} not started`)}")]`,
|
|
963
|
+
` DS -->|"next case"| WORKERS`,
|
|
964
|
+
` subgraph WORKERS["workers · ${t.busy()} of ${t.workers.length} busy"]`,
|
|
965
|
+
' direction LR',
|
|
966
|
+
];
|
|
967
|
+
click('DS', join(t.root, DATASET_DIR), 'the dataset');
|
|
968
|
+
const paused = applying ? 'paused · fixes' : t.drained ? 'nothing left to start' : 'idle';
|
|
969
|
+
for (const w of t.workers) {
|
|
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;
|
|
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');
|
|
988
|
+
}
|
|
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');
|
|
1026
|
+
}
|
|
1027
|
+
click('MG', merged.at(-1)?.dir, 'the latest merged graph');
|
|
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
|
+
}
|
|
1059
|
+
return [
|
|
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>',
|
|
1077
|
+
'',
|
|
1078
|
+
'Click a box to open it.',
|
|
1079
|
+
'',
|
|
1080
|
+
...out,
|
|
1081
|
+
];
|
|
1082
|
+
}
|
|
1083
|
+
function summarySection(spent, tuning, memory) {
|
|
1084
|
+
const phases = ['nomem', 'mem'].filter((p) => spent[p].runs > 0);
|
|
1085
|
+
if (phases.length === 0) {
|
|
1086
|
+
return [];
|
|
1087
|
+
}
|
|
1088
|
+
const row = (label, runs, f) => `| ${label} | ${runs} | ${since(f.wallMs)} | ${f.calls} | ${f.tools} | ${short(f.input)} | ${short(f.cached)} | ${short(f.output)} | ${short(Math.round(tokensIn(f) / runs))} / ${since(f.wallMs / runs)} |`;
|
|
1089
|
+
const out = [
|
|
1090
|
+
'## Summary',
|
|
1091
|
+
'',
|
|
1092
|
+
`Trend compares totals over the same cases; within ${SAME}% is \`same\`. Run time is the wall clock of \`zen run\`.`,
|
|
1093
|
+
'',
|
|
1094
|
+
'| Phase | Runs | Run time | LLM calls | Tool calls | Input | Cached | Output | Per run: tokens / time |',
|
|
1095
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
1096
|
+
...phases.map((p) => row(PHASE_WORD[p], spent[p].runs, spent[p].f)),
|
|
1097
|
+
...(phases.length > 1
|
|
1098
|
+
? [
|
|
1099
|
+
row('**total**', spent.nomem.runs + spent.mem.runs, addFlat(spent.nomem.f, spent.mem.f)),
|
|
1100
|
+
]
|
|
1101
|
+
: []),
|
|
1102
|
+
'',
|
|
1103
|
+
];
|
|
1104
|
+
if (tuning.passed > 0) {
|
|
1105
|
+
const fixed = tuning.passed - tuning.first;
|
|
1106
|
+
out.push('### Tuning effect - first try without memory → passing try', '', `${tuning.passed} case(s) passed without memory, ${tuning.first} on the first try.${fixed > 0 ? ` The ${fixed} that needed fixes, totalled:` : ''}`, '', ...(fixed > 0
|
|
1107
|
+
? compareTable('First try', 'Passing try', tuning.before, tuning.after)
|
|
1108
|
+
: []));
|
|
1109
|
+
}
|
|
1110
|
+
if (memory.cases > 0) {
|
|
1111
|
+
out.push('### Memory effect - passing try without memory → passing try with memory', '', `${memory.cases} completed case(s), cheaper in tokens with memory in ${memory.cheaper}. Totalled:`, '', ...compareTable('Without memory', 'With memory', memory.before, memory.after));
|
|
1112
|
+
}
|
|
1113
|
+
return out;
|
|
1114
|
+
}
|
|
1115
|
+
function slowStatus(t) {
|
|
1116
|
+
const here = t.dir;
|
|
1117
|
+
const q = t.improvements;
|
|
1118
|
+
const cases = selection(t);
|
|
1119
|
+
const store = DatasetStore.open(t.root);
|
|
1120
|
+
const byCase = new Map();
|
|
1121
|
+
const counts = {
|
|
1122
|
+
queued: 0,
|
|
1123
|
+
nomem: 0,
|
|
1124
|
+
mem: 0,
|
|
1125
|
+
completed: 0,
|
|
1126
|
+
difficult: 0,
|
|
1127
|
+
failed: 0,
|
|
1128
|
+
stopped: 0,
|
|
1129
|
+
};
|
|
1130
|
+
const rows = [];
|
|
1131
|
+
const difficult = [];
|
|
1132
|
+
const rubricProblems = [];
|
|
1133
|
+
const failed = [];
|
|
1134
|
+
let everything = {};
|
|
1135
|
+
const spent = {
|
|
1136
|
+
nomem: { runs: 0, f: blank() },
|
|
1137
|
+
mem: { runs: 0, f: blank() },
|
|
1138
|
+
};
|
|
1139
|
+
const tuning = { passed: 0, first: 0, before: blank(), after: blank() };
|
|
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`;
|
|
1159
|
+
for (const c of cases) {
|
|
1160
|
+
const w = t.workers.find((x) => x.case?.id === c.id);
|
|
1161
|
+
const parked = t.improvements.pending.find((f) => f.case === c.id) ??
|
|
1162
|
+
t.improvements.inApply.find((f) => f.case === c.id);
|
|
1163
|
+
const r = resultOf(t, c);
|
|
1164
|
+
const attempts = attemptsOf(t, c);
|
|
1165
|
+
if (r) {
|
|
1166
|
+
counts[r.state]++;
|
|
1167
|
+
}
|
|
1168
|
+
else if (w) {
|
|
1169
|
+
counts[w.phase ?? 'nomem']++;
|
|
1170
|
+
}
|
|
1171
|
+
else if (parked) {
|
|
1172
|
+
counts[parked.phase]++;
|
|
1173
|
+
}
|
|
1174
|
+
else if (attempts.length > 0 && !t.claimed.has(c.id)) {
|
|
1175
|
+
counts.stopped++;
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
counts.queued++;
|
|
1179
|
+
}
|
|
1180
|
+
const nomem = attempts.filter((a) => a.phase === 'nomem');
|
|
1181
|
+
const mem = attempts.filter((a) => a.phase === 'mem');
|
|
1182
|
+
const last = [...attempts].reverse().find((a) => a.feedback);
|
|
1183
|
+
const calls = nomem.map((a) => a.run?.turns).filter((n) => n !== undefined);
|
|
1184
|
+
const memCalls = mem.map((a) => a.run?.turns).filter((n) => n !== undefined);
|
|
1185
|
+
const caseBy = caseTokens(t, c);
|
|
1186
|
+
byCase.set(`${c.id}@${c.rev}`, caseBy);
|
|
1187
|
+
everything = mergeStages(everything, caseBy);
|
|
1188
|
+
for (const a of attempts) {
|
|
1189
|
+
const f = runFlat(a.run);
|
|
1190
|
+
if (f) {
|
|
1191
|
+
spent[a.phase].runs++;
|
|
1192
|
+
spent[a.phase].f = addFlat(spent[a.phase].f, f);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
const passed = passedWithout(attempts);
|
|
1196
|
+
const firstFlat = runFlat(nomem[0]?.run);
|
|
1197
|
+
const passedFlat = runFlat(passed?.run);
|
|
1198
|
+
if (passed) {
|
|
1199
|
+
tuning.passed++;
|
|
1200
|
+
}
|
|
1201
|
+
if (passed?.attempt === 1) {
|
|
1202
|
+
tuning.first++;
|
|
1203
|
+
}
|
|
1204
|
+
else if (firstFlat && passedFlat) {
|
|
1205
|
+
tuning.before = addFlat(tuning.before, firstFlat);
|
|
1206
|
+
tuning.after = addFlat(tuning.after, passedFlat);
|
|
1207
|
+
}
|
|
1208
|
+
const withMem = runFlat(r?.state === 'completed'
|
|
1209
|
+
? mem.find((a) => a.feedback?.done)?.run
|
|
1210
|
+
: latestWith(attempts)?.run);
|
|
1211
|
+
if (r?.state === 'completed' && withMem && passedFlat) {
|
|
1212
|
+
memory.cases++;
|
|
1213
|
+
memory.cheaper += tokensIn(withMem) < tokensIn(passedFlat) ? 1 : 0;
|
|
1214
|
+
memory.before = addFlat(memory.before, passedFlat);
|
|
1215
|
+
memory.after = addFlat(memory.after, withMem);
|
|
1216
|
+
}
|
|
1217
|
+
const effect = withMem && passedFlat
|
|
1218
|
+
? `${change('tokens', tokensIn(withMem), tokensIn(passedFlat))} tokens · ${change('time', withMem.wallMs, passedFlat.wallMs)} time${r?.state === 'completed' ? '' : ' (so far)'}`
|
|
1219
|
+
: '-';
|
|
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}` : '-'} |`);
|
|
1221
|
+
if (r?.state === 'difficult') {
|
|
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(' / '))} |`);
|
|
1230
|
+
}
|
|
1231
|
+
if (r?.state === 'failed') {
|
|
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))} |`);
|
|
1233
|
+
}
|
|
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;
|
|
1316
|
+
const done = counts.completed + counts.difficult;
|
|
1317
|
+
const bar = s.total ? Math.round((done / s.total) * 20) : 0;
|
|
1318
|
+
const state = t.finished ? 'finished' : t.stopping ? 'stopping' : 'running';
|
|
1319
|
+
const q = t.improvements;
|
|
1320
|
+
const out = [
|
|
1321
|
+
`# Fine-tuning - ${basename(t.root)}`,
|
|
1322
|
+
'',
|
|
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)}`,
|
|
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`,
|
|
1325
|
+
'',
|
|
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` : ''}`,
|
|
1327
|
+
'',
|
|
1328
|
+
...s.summary,
|
|
1329
|
+
'## Overview',
|
|
1330
|
+
'',
|
|
1331
|
+
...pathDiagram(t, counts),
|
|
1332
|
+
'',
|
|
1333
|
+
'## System',
|
|
1334
|
+
'',
|
|
1335
|
+
...systemDiagram(t, counts, s.lastApply),
|
|
1336
|
+
'',
|
|
1337
|
+
'## Workers',
|
|
1338
|
+
'',
|
|
1339
|
+
'| Worker | Case | Class | Phase | Try | Step | Since | Run | Analysis | Session | Now |',
|
|
1340
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
1341
|
+
];
|
|
1342
|
+
for (const w of t.workers) {
|
|
1343
|
+
if (!w.case) {
|
|
1344
|
+
out.push(`| ${w.slot} | - | - | - | - | ${idleWord(t)} | - | - | - | - | - |`);
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const dir = t.workDir({ ...w, case: w.case });
|
|
1348
|
+
const run = readJson(join(dir, 'run.json'));
|
|
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
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
out.push('', `## Improvement queue - ${q.pending.length} of ${t.config.applyAt} parked`, '');
|
|
1369
|
+
out.push(q.applying
|
|
1370
|
+
? `Applying now: no new run starts until it is done.${applierNow(t) ? ` Now: ${liveCell(applierNow(t))}` : ''}`
|
|
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.`, '');
|
|
1372
|
+
if (q.pending.length > 0) {
|
|
1373
|
+
out.push('| Request | Case | Phase | Try | Improvements | Summary |', '| --- | --- | --- | --- | --- | --- |');
|
|
1374
|
+
for (const f of q.pending) {
|
|
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))} |`);
|
|
1376
|
+
}
|
|
1377
|
+
out.push('');
|
|
1378
|
+
}
|
|
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)));
|
|
1381
|
+
if (tokenRows.length > 0) {
|
|
1382
|
+
out.push('## Tokens', '', 'Every try of every case so far, every recheck, and every apply - the steps under way included.', '', ...tokenRows);
|
|
1383
|
+
}
|
|
1384
|
+
out.push(...s.rest);
|
|
1385
|
+
out.push('## Recent events', '');
|
|
1386
|
+
for (const e of t.recent.slice(0, 20)) {
|
|
1387
|
+
const who = [
|
|
1388
|
+
e.worker ? `worker ${e.worker}` : '',
|
|
1389
|
+
e.case ?? '',
|
|
1390
|
+
e.phase ? `${PHASE_WORD[e.phase]}${e.attempt ? ` try ${e.attempt}` : ''}` : '',
|
|
1391
|
+
].filter(Boolean);
|
|
1392
|
+
out.push(`- ${e.at.slice(11, 19)} · ${[...who, e.what].join(' · ')}${e.detail ? ` - ${cell(e.detail)}` : ''}`);
|
|
1393
|
+
}
|
|
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`);
|
|
1396
|
+
}
|
|
1397
|
+
//# sourceMappingURL=report.js.map
|