@zenera/cli 1.1.23 → 1.1.25
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 +88 -2
- package/dist/ask.d.ts +51 -0
- package/dist/ask.js +166 -0
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +83 -0
- package/dist/batch.js +413 -0
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +80 -7
- package/dist/command.d.ts +6 -0
- package/dist/commands/inspect.d.ts +57 -0
- package/dist/commands/inspect.js +539 -91
- package/dist/commands/list.js +128 -19
- package/dist/commands/memory.js +461 -32
- package/dist/commands/meta.js +311 -21
- package/dist/commands/models.js +110 -14
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +175 -36
- package/dist/commands/sandbox.js +148 -15
- package/dist/engine.d.ts +24 -0
- package/dist/engine.js +61 -8
- package/dist/eventlog.d.ts +59 -0
- package/dist/eventlog.js +146 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +20 -3
- package/dist/main.js +5 -2
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +60 -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 +338 -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 +125 -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 +45 -0
- package/dist/meta/finetune/feedback.js +85 -0
- package/dist/meta/finetune/improvements.d.ts +51 -0
- package/dist/meta/finetune/improvements.js +244 -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 +74 -0
- package/dist/meta/finetune/live.js +226 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +114 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/report.d.ts +33 -0
- package/dist/meta/finetune/report.js +1284 -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 +41 -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 +39 -0
- package/dist/meta/finetune/train.js +391 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +291 -0
- package/dist/meta/finetune/tuning.d.ts +167 -0
- package/dist/meta/finetune/tuning.js +289 -0
- package/dist/meta/finetune/usage.d.ts +62 -0
- package/dist/meta/finetune/usage.js +225 -0
- package/dist/meta/host.d.ts +14 -0
- package/dist/meta/host.js +20 -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/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +42 -0
- package/dist/podman.js +198 -5
- package/dist/progress.d.ts +33 -0
- package/dist/progress.js +402 -0
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/request.d.ts +45 -0
- package/dist/request.js +300 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +20 -3
- package/dist/scaffold.d.ts +15 -6
- package/dist/scaffold.js +5 -60
- package/dist/session.d.ts +26 -1
- package/dist/session.js +46 -32
- package/dist/term.d.ts +5 -0
- package/dist/term.js +28 -3
- package/dist/trace.d.ts +84 -0
- package/dist/trace.js +737 -0
- package/dist/tui/models.d.ts +33 -0
- package/dist/tui/models.js +389 -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/dist/validate.js +25 -3
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +41 -107
- package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
- 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 +4 -3
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
- package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
- package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
- 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 +1791 -0
- 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 +431 -0
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
- package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
- package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
- package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/agents.schema.json +1 -1
- package/templates/editor/.vscode/settings.json +2 -0
- package/templates/project/agents/files-instructions.md +35 -13
- package/templates/project/agents/fork-instructions.md +27 -0
- package/templates/project/agents/memory-instructions.md +106 -34
- package/templates/project/agents/tools-instructions.md +7 -4
- package/templates/project/gitignore +5 -0
- package/dist/meta.d.ts +0 -221
- package/dist/meta.js +0 -684
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
import { MERMAID_URL } from '@zenera/neo';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
4
|
+
import { safe } from "../host.js";
|
|
5
|
+
import { DatasetStore } from "./dataset/store.js";
|
|
6
|
+
import { DATASET_DIR } from "./dataset/types.js";
|
|
7
|
+
import { feedbackId, readFeedback } from "./feedback.js";
|
|
8
|
+
import { resultOf, selection } from "./sampler.js";
|
|
9
|
+
import { readJson } from "./tuning.js";
|
|
10
|
+
import { 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
|
+
/** A link from `from` (a directory) to `to`, safe for spaces. */
|
|
23
|
+
const link = (label, from, to) => `[${label}](<${relative(from, to).split(sep).join('/')}>)`;
|
|
24
|
+
export function since(ms) {
|
|
25
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
26
|
+
if (s < 60) {
|
|
27
|
+
return `${s}s`;
|
|
28
|
+
}
|
|
29
|
+
const m = Math.floor(s / 60);
|
|
30
|
+
if (m < 60) {
|
|
31
|
+
return `${m}m ${String(s % 60).padStart(2, '0')}s`;
|
|
32
|
+
}
|
|
33
|
+
return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`;
|
|
34
|
+
}
|
|
35
|
+
export function attemptsOf(t, c) {
|
|
36
|
+
const dir = t.caseDir(c);
|
|
37
|
+
let names = [];
|
|
38
|
+
try {
|
|
39
|
+
names = readdirSync(dir).filter((n) => /^\d{2}-(nomem|mem)$/.test(n));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return names
|
|
45
|
+
.map((name) => {
|
|
46
|
+
const [n, phase] = name.split('-');
|
|
47
|
+
const at = join(dir, name);
|
|
48
|
+
const read = readFeedback(join(at, 'feedback.json'), t.known(c, phase, Number(n)));
|
|
49
|
+
return {
|
|
50
|
+
phase,
|
|
51
|
+
attempt: Number(n),
|
|
52
|
+
dir: at,
|
|
53
|
+
run: readJson(join(at, 'run.json')),
|
|
54
|
+
...('feedback' in read ? { feedback: read.feedback } : {}),
|
|
55
|
+
};
|
|
56
|
+
})
|
|
57
|
+
.sort((a, b) => Number(a.phase === 'mem') - Number(b.phase === 'mem') || a.attempt - b.attempt);
|
|
58
|
+
}
|
|
59
|
+
function decisions(t) {
|
|
60
|
+
const all = {};
|
|
61
|
+
for (const a of t.improvements.applies()) {
|
|
62
|
+
Object.assign(all, readJson(join(a.dir, 'decisions.json')) ?? {});
|
|
63
|
+
}
|
|
64
|
+
return all;
|
|
65
|
+
}
|
|
66
|
+
/** A run's tokens per model: recorded at the run, or read from its trajectory. */
|
|
67
|
+
function runByModel(run) {
|
|
68
|
+
if (!run) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const by = run.tokens ?? runTokens(run.dir);
|
|
72
|
+
if (by && Object.keys(by).length > 0) {
|
|
73
|
+
return by;
|
|
74
|
+
}
|
|
75
|
+
return run.usage
|
|
76
|
+
? {
|
|
77
|
+
'?': {
|
|
78
|
+
calls: run.turns ?? 0,
|
|
79
|
+
input: run.usage.inputTokens,
|
|
80
|
+
cached: run.usage.cachedInputTokens ?? 0,
|
|
81
|
+
output: run.usage.outputTokens,
|
|
82
|
+
reasoning: run.usage.reasoningTokens ?? 0,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Every try of a case, per stage and model. Applies serve many cases at once, so they are not split. */
|
|
88
|
+
export function caseTokens(t, c) {
|
|
89
|
+
const attempts = attemptsOf(t, c);
|
|
90
|
+
return mergeStages(...attempts.map((a) => ({
|
|
91
|
+
run: runByModel(a.run),
|
|
92
|
+
analyze: envelopesIn(a.dir, 'analyze'),
|
|
93
|
+
})));
|
|
94
|
+
}
|
|
95
|
+
/** `in / out`, the input counting cached tokens. */
|
|
96
|
+
const inOut = (by) => {
|
|
97
|
+
const s = total(by);
|
|
98
|
+
return s.input + s.output > 0 ? `${short(s.input)} / ${short(s.output)}` : '-';
|
|
99
|
+
};
|
|
100
|
+
function tokenTable(by) {
|
|
101
|
+
const rows = [];
|
|
102
|
+
for (const stage of STAGES) {
|
|
103
|
+
const models = Object.entries(by[stage] ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
104
|
+
for (const [model, s] of models) {
|
|
105
|
+
rows.push(`| ${stage} | ${cell(model)} | ${s.calls} | ${short(s.input)} | ${short(s.cached)} | ${short(s.output)} | ${short(s.reasoning)} |`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (rows.length === 0) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
'| Stage | Model | Calls | Input | Cached | Output | Reasoning |',
|
|
113
|
+
'| --- | --- | --- | --- | --- | --- | --- |',
|
|
114
|
+
...rows,
|
|
115
|
+
'',
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
const rubricScore = (f) => {
|
|
119
|
+
const r = Object.values(f?.rubric ?? {});
|
|
120
|
+
return r.length ? `${r.filter((x) => x === 'pass').length}/${r.length}` : '-';
|
|
121
|
+
};
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// What a run did, and how it moved from the try it is compared with
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
const FLAT_KEYS = [
|
|
126
|
+
'calls',
|
|
127
|
+
'input',
|
|
128
|
+
'cached',
|
|
129
|
+
'output',
|
|
130
|
+
'reasoning',
|
|
131
|
+
'peak',
|
|
132
|
+
'llmMs',
|
|
133
|
+
'wallMs',
|
|
134
|
+
'toolMs',
|
|
135
|
+
'tools',
|
|
136
|
+
'toolErrors',
|
|
137
|
+
'recall',
|
|
138
|
+
'load',
|
|
139
|
+
'grep',
|
|
140
|
+
'commit',
|
|
141
|
+
'forget',
|
|
142
|
+
'forks',
|
|
143
|
+
'branches',
|
|
144
|
+
'compactions',
|
|
145
|
+
'handoffs',
|
|
146
|
+
];
|
|
147
|
+
const blank = () => Object.fromEntries(FLAT_KEYS.map((k) => [k, 0]));
|
|
148
|
+
function addFlat(...all) {
|
|
149
|
+
const out = blank();
|
|
150
|
+
for (const f of all) {
|
|
151
|
+
for (const k of FLAT_KEYS) {
|
|
152
|
+
out[k] += f?.[k] ?? 0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** Recorded at the run, or read from its trajectory when the run predates that. */
|
|
158
|
+
const metricsOf = (run) => run ? (run.metrics ?? runMetrics(run.dir)) : undefined;
|
|
159
|
+
const modelFlat = (x) => ({
|
|
160
|
+
...blank(),
|
|
161
|
+
...(x
|
|
162
|
+
? {
|
|
163
|
+
calls: x.calls,
|
|
164
|
+
input: x.input,
|
|
165
|
+
cached: x.cached,
|
|
166
|
+
output: x.output,
|
|
167
|
+
reasoning: x.reasoning,
|
|
168
|
+
peak: x.peak,
|
|
169
|
+
llmMs: x.ms,
|
|
170
|
+
}
|
|
171
|
+
: {}),
|
|
172
|
+
});
|
|
173
|
+
function runFlat(run) {
|
|
174
|
+
const m = metricsOf(run);
|
|
175
|
+
if (!run || !m) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const models = Object.values(m.models);
|
|
179
|
+
return {
|
|
180
|
+
...addFlat(...models.map(modelFlat)),
|
|
181
|
+
peak: Math.max(0, ...models.map((x) => x.peak)),
|
|
182
|
+
llmMs: m.llmMs,
|
|
183
|
+
wallMs: run.durationMs ?? 0,
|
|
184
|
+
toolMs: m.toolMs,
|
|
185
|
+
tools: m.toolCalls,
|
|
186
|
+
toolErrors: m.toolErrors,
|
|
187
|
+
...m.memory,
|
|
188
|
+
forks: m.forks,
|
|
189
|
+
branches: m.branches,
|
|
190
|
+
compactions: m.compactions,
|
|
191
|
+
handoffs: m.handoffs,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const tokensIn = (f) => f.input + f.output;
|
|
195
|
+
const MODEL_METRICS = [
|
|
196
|
+
{ label: 'LLM calls', kind: 'count', better: 'lower', of: (f) => f.calls },
|
|
197
|
+
{ label: 'Input', kind: 'tokens', better: 'lower', of: (f) => f.input },
|
|
198
|
+
{ label: 'Cached', kind: 'tokens', of: (f) => f.cached },
|
|
199
|
+
{ label: 'Uncached input', kind: 'tokens', better: 'lower', of: (f) => f.input - f.cached },
|
|
200
|
+
{
|
|
201
|
+
label: 'Cache hit',
|
|
202
|
+
kind: 'share',
|
|
203
|
+
better: 'higher',
|
|
204
|
+
of: (f) => (f.input ? (f.cached / f.input) * 100 : 0),
|
|
205
|
+
},
|
|
206
|
+
{ label: 'Output', kind: 'tokens', better: 'lower', of: (f) => f.output },
|
|
207
|
+
{ label: 'Reasoning', kind: 'tokens', better: 'lower', of: (f) => f.reasoning },
|
|
208
|
+
{ label: 'Peak input', kind: 'tokens', better: 'lower', of: (f) => f.peak },
|
|
209
|
+
{ label: 'LLM time', kind: 'time', better: 'lower', of: (f) => f.llmMs },
|
|
210
|
+
];
|
|
211
|
+
const RUN_METRICS = [
|
|
212
|
+
{ label: 'Wall clock', kind: 'time', better: 'lower', of: (f) => f.wallMs },
|
|
213
|
+
...MODEL_METRICS,
|
|
214
|
+
{ label: 'Tool calls', kind: 'count', better: 'lower', of: (f) => f.tools },
|
|
215
|
+
{ label: 'Tool errors', kind: 'count', better: 'lower', of: (f) => f.toolErrors },
|
|
216
|
+
{ label: 'Tool time', kind: 'time', better: 'lower', of: (f) => f.toolMs },
|
|
217
|
+
{ label: 'Memory recalls', kind: 'count', of: (f) => f.recall },
|
|
218
|
+
{ label: 'Memory loads', kind: 'count', of: (f) => f.load },
|
|
219
|
+
{ label: 'Memory greps', kind: 'count', of: (f) => f.grep },
|
|
220
|
+
{ label: 'Memory commits', kind: 'count', of: (f) => f.commit },
|
|
221
|
+
{ label: 'Memory forgets', kind: 'count', of: (f) => f.forget },
|
|
222
|
+
{ label: 'Forks', kind: 'count', of: (f) => f.forks },
|
|
223
|
+
{ label: 'Branches', kind: 'count', of: (f) => f.branches },
|
|
224
|
+
{ label: 'Compactions', kind: 'count', of: (f) => f.compactions },
|
|
225
|
+
{ label: 'Handoffs', kind: 'count', of: (f) => f.handoffs },
|
|
226
|
+
];
|
|
227
|
+
const SUMMARY_METRICS = RUN_METRICS.filter((m) => [
|
|
228
|
+
'Wall clock',
|
|
229
|
+
'LLM calls',
|
|
230
|
+
'Tool calls',
|
|
231
|
+
'Tool errors',
|
|
232
|
+
'Input',
|
|
233
|
+
'Uncached input',
|
|
234
|
+
'Cache hit',
|
|
235
|
+
'Output',
|
|
236
|
+
'LLM time',
|
|
237
|
+
'Tool time',
|
|
238
|
+
].includes(m.label));
|
|
239
|
+
function show(kind, n) {
|
|
240
|
+
switch (kind) {
|
|
241
|
+
case 'tokens':
|
|
242
|
+
return short(n);
|
|
243
|
+
case 'time':
|
|
244
|
+
return since(n);
|
|
245
|
+
case 'share':
|
|
246
|
+
return `${Math.round(n)}%`;
|
|
247
|
+
default:
|
|
248
|
+
return String(n);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Percent for amounts, points for a share, a plain difference for a count unless `relative`. */
|
|
252
|
+
function change(kind, now, before, relative = kind !== 'count') {
|
|
253
|
+
const signed = (d, unit) => d === 0 ? '=' : `${d > 0 ? '+' : ''}${d}${unit}`;
|
|
254
|
+
if (kind === 'share') {
|
|
255
|
+
return signed(Math.round(now - before), 'pt');
|
|
256
|
+
}
|
|
257
|
+
if (!relative) {
|
|
258
|
+
return signed(now - before, '');
|
|
259
|
+
}
|
|
260
|
+
if (before === 0) {
|
|
261
|
+
return now === 0 ? '=' : 'new';
|
|
262
|
+
}
|
|
263
|
+
return signed(Math.round(((now - before) / before) * 100), '%');
|
|
264
|
+
}
|
|
265
|
+
/** Within this many percent (points, for a share) is noise, not a trend. */
|
|
266
|
+
const SAME = 5;
|
|
267
|
+
function trend(m, now, before) {
|
|
268
|
+
if (!m.better) {
|
|
269
|
+
return '';
|
|
270
|
+
}
|
|
271
|
+
const d = m.kind === 'share'
|
|
272
|
+
? now - before
|
|
273
|
+
: before === 0
|
|
274
|
+
? Math.sign(now) * Infinity
|
|
275
|
+
: ((now - before) / before) * 100;
|
|
276
|
+
if (Number.isNaN(d) || Math.abs(d) < SAME) {
|
|
277
|
+
return 'same';
|
|
278
|
+
}
|
|
279
|
+
return d < 0 === (m.better === 'lower') ? 'better' : 'worse';
|
|
280
|
+
}
|
|
281
|
+
/** Rows are metrics, columns tries; a metric that is zero everywhere is left out. */
|
|
282
|
+
function matrix(metrics, cols, effect) {
|
|
283
|
+
const all = [...cols.map((c) => c.now), effect?.now, effect?.base];
|
|
284
|
+
const shown = metrics.filter((m) => all.some((f) => f && m.of(f) !== 0));
|
|
285
|
+
if (shown.length === 0) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const both = effect?.now && effect.base ? { now: effect.now, base: effect.base } : undefined;
|
|
289
|
+
const head = ['Metric', ...cols.map((c) => c.label), ...(both ? ['Memory effect'] : [])];
|
|
290
|
+
const rows = shown.map((m) => {
|
|
291
|
+
const cells = cols.map((c) => {
|
|
292
|
+
if (!c.now) {
|
|
293
|
+
return '-';
|
|
294
|
+
}
|
|
295
|
+
const v = show(m.kind, m.of(c.now));
|
|
296
|
+
return c.base ? `${v} (${change(m.kind, m.of(c.now), m.of(c.base))})` : v;
|
|
297
|
+
});
|
|
298
|
+
if (both) {
|
|
299
|
+
const now = m.of(both.now);
|
|
300
|
+
const before = m.of(both.base);
|
|
301
|
+
const word = trend(m, now, before);
|
|
302
|
+
cells.push(`${change(m.kind, now, before, true)}${word ? ` ${word}` : ''}`);
|
|
303
|
+
}
|
|
304
|
+
return `| ${m.label} | ${cells.join(' | ')} |`;
|
|
305
|
+
});
|
|
306
|
+
return [`| ${head.join(' | ')} |`, `| ${head.map(() => '---').join(' | ')} |`, ...rows, ''];
|
|
307
|
+
}
|
|
308
|
+
/** The no-memory try that passed: the one every try with memory is measured against. */
|
|
309
|
+
const passedWithout = (attempts) => attempts.find((a) => a.phase === 'nomem' && a.feedback?.done);
|
|
310
|
+
const latestWith = (attempts) => [...attempts].reverse().find((a) => a.phase === 'mem' && a.run);
|
|
311
|
+
/** Each try against the one before it in its phase; the first with memory against the passing one without. */
|
|
312
|
+
function tryColumns(attempts, flat) {
|
|
313
|
+
const passed = passedWithout(attempts);
|
|
314
|
+
const cols = attempts.map((a, i) => {
|
|
315
|
+
const base = attempts
|
|
316
|
+
.slice(0, i)
|
|
317
|
+
.reverse()
|
|
318
|
+
.find((b) => b.phase === a.phase) ?? (a.phase === 'mem' ? passed : undefined);
|
|
319
|
+
return {
|
|
320
|
+
label: `${PHASE_WORD[a.phase]}, try ${a.attempt}`,
|
|
321
|
+
now: flat(a),
|
|
322
|
+
base: base && flat(base),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
const mem = latestWith(attempts);
|
|
326
|
+
return {
|
|
327
|
+
cols,
|
|
328
|
+
...(passed && mem ? { effect: { now: flat(mem), base: flat(passed) } } : {}),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** Before and after, one row per metric, with whether it moved the right way. */
|
|
332
|
+
function compareTable(before, after, a, b) {
|
|
333
|
+
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)) || '-'} |`);
|
|
334
|
+
return [
|
|
335
|
+
`| Metric | ${before} | ${after} | Change | Trend |`,
|
|
336
|
+
'| --- | --- | --- | --- | --- |',
|
|
337
|
+
...rows,
|
|
338
|
+
'',
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
function stateOf(t, c) {
|
|
342
|
+
const r = resultOf(t, c);
|
|
343
|
+
if (r) {
|
|
344
|
+
if (r.state === 'failed') {
|
|
345
|
+
return `failed (${r.reason ?? r.phase}) - resumes at the next start`;
|
|
346
|
+
}
|
|
347
|
+
return r.state === 'completed' ? 'completed' : `difficult (${r.reason ?? r.phase})`;
|
|
348
|
+
}
|
|
349
|
+
const w = t.workers.find((x) => x.case?.id === c.id);
|
|
350
|
+
if (w) {
|
|
351
|
+
return `${PHASE_WORD[w.phase ?? 'nomem']} · try ${w.attempt} · ${w.step}`;
|
|
352
|
+
}
|
|
353
|
+
const parked = t.improvements.pending.find((f) => f.case === c.id);
|
|
354
|
+
if (parked) {
|
|
355
|
+
return `${PHASE_WORD[parked.phase]} · try ${parked.attempt} · parked`;
|
|
356
|
+
}
|
|
357
|
+
const applying = t.improvements.inApply.find((f) => f.case === c.id);
|
|
358
|
+
if (applying) {
|
|
359
|
+
return `${PHASE_WORD[applying.phase]} · try ${applying.attempt} · parked, being applied`;
|
|
360
|
+
}
|
|
361
|
+
if (t.claimed.has(c.id)) {
|
|
362
|
+
return 'waiting for a worker';
|
|
363
|
+
}
|
|
364
|
+
return attemptsOf(t, c).length > 0 ? 'stopped' : 'queued';
|
|
365
|
+
}
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// FEEDBACK.md - one case
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
export function feedbackPath(t, c) {
|
|
370
|
+
return join(t.dir, 'cases', c.id, 'FEEDBACK.md');
|
|
371
|
+
}
|
|
372
|
+
export function writeFeedback(t, c) {
|
|
373
|
+
const file = feedbackPath(t, c);
|
|
374
|
+
const here = dirname(file);
|
|
375
|
+
const attempts = attemptsOf(t, c);
|
|
376
|
+
const latest = [...attempts].reverse().find((a) => a.feedback);
|
|
377
|
+
const decided = decisions(t);
|
|
378
|
+
const session = existsSync(join(t.caseDir(c), 'session'))
|
|
379
|
+
? readFileSync(join(t.caseDir(c), 'session'), 'utf8').trim()
|
|
380
|
+
: undefined;
|
|
381
|
+
const lines = [];
|
|
382
|
+
const say = (...more) => {
|
|
383
|
+
lines.push(...more);
|
|
384
|
+
};
|
|
385
|
+
say(`# ${c.id}`, '', `${link('← status', here, join(t.dir, 'STATUS.md'))}`, '');
|
|
386
|
+
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}\`` : '-'} |`, '');
|
|
387
|
+
if (c.rubric.length > 0) {
|
|
388
|
+
say('## Rubric', '', ...c.rubric.map((r) => `- **${r.id}** ${r.text}`), '');
|
|
389
|
+
}
|
|
390
|
+
const input = typeof c.input === 'string'
|
|
391
|
+
? c.input
|
|
392
|
+
: c.input
|
|
393
|
+
.map((p) => typeof p === 'string' ? p : 'text' in p ? p.text : `[${Object.keys(p)[0]}]`)
|
|
394
|
+
.join(' ');
|
|
395
|
+
say('## Input', '', ...input
|
|
396
|
+
.slice(0, 800)
|
|
397
|
+
.split('\n')
|
|
398
|
+
.map((l) => `> ${l}`), '');
|
|
399
|
+
if (latest?.feedback) {
|
|
400
|
+
const f = latest.feedback;
|
|
401
|
+
say(`## Latest analysis - ${PHASE_WORD[latest.phase]}, try ${latest.attempt}: ${f.verdict ?? '?'}, rubric ${rubricScore(f)}${f.done ? ', done' : ''}`, '');
|
|
402
|
+
if (f.summary) {
|
|
403
|
+
say(`> ${cell(f.summary)}`, '');
|
|
404
|
+
}
|
|
405
|
+
const id = feedbackId(c.id, latest.phase, latest.attempt, t.round(c));
|
|
406
|
+
const improvements = f.improvements ?? [];
|
|
407
|
+
if (improvements.length > 0) {
|
|
408
|
+
say('### Improvements', '', '| # | Change | File | Decision |', '| --- | --- | --- | --- |');
|
|
409
|
+
for (const i of improvements) {
|
|
410
|
+
const d = decided[id]?.[i.id];
|
|
411
|
+
const decision = d?.decision
|
|
412
|
+
? `${d.decision}${d.note ? `: ${d.note}` : ''}`
|
|
413
|
+
: t.improvements.isApplied(id)
|
|
414
|
+
? 'applied (no decision recorded)'
|
|
415
|
+
: 'waiting for an apply';
|
|
416
|
+
say(`| ${cell(i.id)} | ${cell(i.change)} | ${cell(i.file)} | ${cell(decision)} |`);
|
|
417
|
+
}
|
|
418
|
+
say('');
|
|
419
|
+
}
|
|
420
|
+
const analysis = join(latest.dir, 'analysis.md');
|
|
421
|
+
if (existsSync(analysis)) {
|
|
422
|
+
say('### The analysis', '', readFileSync(analysis, 'utf8').trim(), '');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (attempts.length > 0) {
|
|
426
|
+
say('## History', '', '| Phase | Try | Verdict | Rubric | LLM calls | Tool calls | Run tokens | Analyze tokens | Seconds | System | Links |', '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
427
|
+
for (const a of [...attempts].reverse()) {
|
|
428
|
+
const links = [
|
|
429
|
+
a.run ? link('run', here, a.run.dir) : '',
|
|
430
|
+
a.run?.report ? link('report', here, a.run.report) : '',
|
|
431
|
+
existsSync(join(a.dir, 'analysis.md'))
|
|
432
|
+
? link('analysis', here, join(a.dir, 'analysis.md'))
|
|
433
|
+
: '',
|
|
434
|
+
existsSync(join(a.dir, 'feedback.json'))
|
|
435
|
+
? link('feedback', here, join(a.dir, 'feedback.json'))
|
|
436
|
+
: '',
|
|
437
|
+
].filter(Boolean);
|
|
438
|
+
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(' · ') || '-'} |`);
|
|
439
|
+
}
|
|
440
|
+
say('');
|
|
441
|
+
}
|
|
442
|
+
const byRun = tryColumns(attempts, (a) => runFlat(a.run));
|
|
443
|
+
const runRows = matrix(RUN_METRICS, byRun.cols, byRun.effect);
|
|
444
|
+
if (runRows.length > 0) {
|
|
445
|
+
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);
|
|
446
|
+
const models = [
|
|
447
|
+
...new Set(attempts.flatMap((a) => Object.keys(metricsOf(a.run)?.models ?? {}))),
|
|
448
|
+
].sort();
|
|
449
|
+
if (models.length > 1) {
|
|
450
|
+
for (const model of models) {
|
|
451
|
+
const per = tryColumns(attempts, (a) => {
|
|
452
|
+
const m = metricsOf(a.run);
|
|
453
|
+
return m && modelFlat(m.models[model]);
|
|
454
|
+
});
|
|
455
|
+
say(`### ${cell(model)}`, '', ...matrix(MODEL_METRICS, per.cols, per.effect));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const byStage = caseTokens(t, c);
|
|
460
|
+
if (Object.keys(byStage).length > 0) {
|
|
461
|
+
say('## Tokens', '', ...tokenTable(byStage));
|
|
462
|
+
}
|
|
463
|
+
const submitted = t.memories.submissions().find((s) => s.key === `${c.id}@r${c.rev}`);
|
|
464
|
+
if (submitted) {
|
|
465
|
+
const merges = t.memories
|
|
466
|
+
.merges()
|
|
467
|
+
.filter((m) => m.ok && m.includes.includes(submitted.key));
|
|
468
|
+
say('## Memory', '', submitted.empty
|
|
469
|
+
? `The passing run with memory (try ${submitted.attempt}) committed nothing, so there was nothing to keep.`
|
|
470
|
+
: `Kept ${submitted.at.slice(0, 16).replace('T', ' ')} from the passing run with memory (try ${submitted.attempt}): ${link('the copy', here, submitted.dir)}.`, '');
|
|
471
|
+
if (!submitted.empty) {
|
|
472
|
+
say(merges.length > 0
|
|
473
|
+
? `In merges: ${merges.map((m) => link(m.name, here, m.dir)).join(', ')}.`
|
|
474
|
+
: 'Not merged yet.', '');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const md = `${lines.join('\n')}\n`;
|
|
478
|
+
atomic(file, md);
|
|
479
|
+
atomic(join(here, 'FEEDBACK.html'), markdownPage(`${c.id} - ${basename(t.root)}`, md, !t.finished && !resultOf(t, c)));
|
|
480
|
+
}
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// APPLY.md - one apply: every request, every decision, every file it changed
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
/** A fence longer than any run of backticks in `text`, so nothing inside can close it. */
|
|
485
|
+
function fence(text) {
|
|
486
|
+
const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((r) => r.length));
|
|
487
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
488
|
+
}
|
|
489
|
+
export function patchFiles(patch) {
|
|
490
|
+
return patch
|
|
491
|
+
.split(/^(?=diff --git )/m)
|
|
492
|
+
.filter((p) => p.startsWith('diff --git '))
|
|
493
|
+
.map((text) => {
|
|
494
|
+
const m = /^diff --git a\/(\S+) b\/(\S+)/.exec(text);
|
|
495
|
+
const file = (m?.[2] ?? '?').replace(/^v\d+\//, '');
|
|
496
|
+
let added = 0;
|
|
497
|
+
let removed = 0;
|
|
498
|
+
for (const line of text.split('\n')) {
|
|
499
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (line.startsWith('+')) {
|
|
503
|
+
added++;
|
|
504
|
+
}
|
|
505
|
+
else if (line.startsWith('-')) {
|
|
506
|
+
removed++;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { file, added, removed, text: text.trimEnd() };
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
export function applyPath(dir) {
|
|
513
|
+
return join(dir, 'APPLY.md');
|
|
514
|
+
}
|
|
515
|
+
export function writeApply(t, dir) {
|
|
516
|
+
const name = basename(dir);
|
|
517
|
+
const here = dir;
|
|
518
|
+
const inputs = readJson(join(dir, 'inputs.json')) ?? [];
|
|
519
|
+
const applied = readJson(join(dir, 'applied.json'));
|
|
520
|
+
const failed = readJson(join(dir, 'failed.json'));
|
|
521
|
+
const decided = readJson(join(dir, 'decisions.json')) ?? {};
|
|
522
|
+
const envelope = readJson(join(dir, 'apply.json'));
|
|
523
|
+
const retried = existsSync(join(dir, 'apply-2.json'));
|
|
524
|
+
const patch = existsSync(join(dir, 'diff.patch'))
|
|
525
|
+
? readFileSync(join(dir, 'diff.patch'), 'utf8')
|
|
526
|
+
: '';
|
|
527
|
+
const files = patchFiles(patch);
|
|
528
|
+
const improvements = inputs.reduce((n, f) => n + f.improvements.length, 0);
|
|
529
|
+
const status = failed
|
|
530
|
+
? 'failed - the project was put back as it was'
|
|
531
|
+
: applied
|
|
532
|
+
? applied.skipped
|
|
533
|
+
? 'nothing to change'
|
|
534
|
+
: 'applied'
|
|
535
|
+
: 'in progress';
|
|
536
|
+
const started = applied?.startedAt ??
|
|
537
|
+
failed?.startedAt ??
|
|
538
|
+
(existsSync(join(dir, 'inputs.json'))
|
|
539
|
+
? statSync(join(dir, 'inputs.json')).mtime.toISOString()
|
|
540
|
+
: undefined);
|
|
541
|
+
const finished = applied?.at ?? failed?.at;
|
|
542
|
+
const when = (iso) => iso ? iso.slice(0, 19).replace('T', ' ') : '-';
|
|
543
|
+
const took = started && finished ? since(Date.parse(finished) - Date.parse(started)) : '-';
|
|
544
|
+
const tally = {};
|
|
545
|
+
for (const f of inputs) {
|
|
546
|
+
for (const i of f.improvements) {
|
|
547
|
+
const d = decided[f.id]?.[i.id]?.decision ?? 'undecided';
|
|
548
|
+
tally[d] = (tally[d] ?? 0) + 1;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const out = [
|
|
552
|
+
`# Apply ${name}${applied ? ` - v${applied.from} → v${applied.to}` : ''}`,
|
|
553
|
+
'',
|
|
554
|
+
link('← status', here, join(t.dir, 'STATUS.md')),
|
|
555
|
+
'',
|
|
556
|
+
'| Status | Started | Finished | Took | Requests | Improvements | Files changed | Retried |',
|
|
557
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
558
|
+
`| ${status} | ${when(started)} | ${when(finished)} | ${took} | ${inputs.length} | ${improvements} | ${files.length} | ${retried ? 'yes - the first attempt failed zen check' : 'no'} |`,
|
|
559
|
+
'',
|
|
560
|
+
];
|
|
561
|
+
if (failed) {
|
|
562
|
+
out.push(`> ${cell(failed.error)}`, '');
|
|
563
|
+
}
|
|
564
|
+
if (envelope?.sessionId) {
|
|
565
|
+
out.push(`The apply session: \`zen meta resume ${basename(t.root)} ${envelope.sessionId}\` - ask it why it changed what it changed.`, '');
|
|
566
|
+
}
|
|
567
|
+
out.push('## Decisions', '', '| Applied | Merged | Rejected | Structural | Undecided |', '| --- | --- | --- | --- | --- |', `| ${tally.applied ?? 0} | ${tally.merged ?? 0} | ${tally.rejected ?? 0} | ${tally.structural ?? 0} | ${tally.undecided ?? 0} |`, '', '## Requests', '');
|
|
568
|
+
inputs.forEach((f, n) => {
|
|
569
|
+
const tryDir = f.file
|
|
570
|
+
? dirname(f.file)
|
|
571
|
+
: t.attemptDir({ id: f.case, rev: f.caseRev }, f.phase, f.attempt);
|
|
572
|
+
const run = readJson(join(tryDir, 'run.json'));
|
|
573
|
+
const links = [
|
|
574
|
+
link('case', here, feedbackPath(t, { id: f.case })),
|
|
575
|
+
existsSync(join(tryDir, 'analysis.md'))
|
|
576
|
+
? link('analysis', here, join(tryDir, 'analysis.md'))
|
|
577
|
+
: '',
|
|
578
|
+
existsSync(join(tryDir, 'feedback.json'))
|
|
579
|
+
? link('feedback', here, join(tryDir, 'feedback.json'))
|
|
580
|
+
: '',
|
|
581
|
+
run ? link('run', here, run.dir) : '',
|
|
582
|
+
run?.report ? link('report', here, run.report) : '',
|
|
583
|
+
].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(' · ')}`, '');
|
|
585
|
+
if (f.summary) {
|
|
586
|
+
out.push(`> ${cell(f.summary)}`, '');
|
|
587
|
+
}
|
|
588
|
+
if (f.improvements.length === 0) {
|
|
589
|
+
out.push('No improvements asked for.', '');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
out.push('| # | Kind | File | Change | Why | Expect | Decision | Note |', '| --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
593
|
+
for (const i of f.improvements) {
|
|
594
|
+
const d = decided[f.id]?.[i.id];
|
|
595
|
+
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)} |`);
|
|
596
|
+
}
|
|
597
|
+
out.push('');
|
|
598
|
+
});
|
|
599
|
+
out.push('## What the apply agent did', '');
|
|
600
|
+
const changes = join(dir, 'changes.md');
|
|
601
|
+
out.push(existsSync(changes)
|
|
602
|
+
? readFileSync(changes, 'utf8').trim().replace(/^#/gm, '###')
|
|
603
|
+
: '_The apply agent wrote no changes.md._', '');
|
|
604
|
+
if (envelope?.answer) {
|
|
605
|
+
out.push('### Its answer', '', ...envelope.answer
|
|
606
|
+
.trim()
|
|
607
|
+
.split('\n')
|
|
608
|
+
.map((l) => `> ${l}`), '');
|
|
609
|
+
}
|
|
610
|
+
out.push('## Files changed', '');
|
|
611
|
+
if (files.length === 0) {
|
|
612
|
+
out.push(applied ? '_None: the system is unchanged._' : '_Not yet._', '');
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
out.push('| File | Added | Removed |', '| --- | --- | --- |', ...files.map((f) => `| \`${f.file}\` | +${f.added} | -${f.removed} |`), '');
|
|
616
|
+
for (const f of files) {
|
|
617
|
+
const tick = fence(f.text);
|
|
618
|
+
out.push(`### \`${f.file}\``, '', `${tick}diff`, f.text, tick, '');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
out.push('## zen check', '');
|
|
622
|
+
const check = join(dir, 'check.log');
|
|
623
|
+
if (existsSync(check)) {
|
|
624
|
+
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';
|
|
632
|
+
const tick = fence(tail);
|
|
633
|
+
out.push(verdict, '', `${tick}text`, tail || '(no output)', tick, '');
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
out.push(applied?.skipped ? '_Not run: nothing was changed._' : '_Not run yet._', '');
|
|
637
|
+
}
|
|
638
|
+
if (applied) {
|
|
639
|
+
out.push('## Cases sent back to run', '', ...inputs.map((f) => `- ${link(f.case, here, feedbackPath(t, { id: f.case }))} - ${PHASE_WORD[f.phase]}, try ${f.attempt + 1} on v${applied.to}`), '');
|
|
640
|
+
}
|
|
641
|
+
atomic(applyPath(dir), `${out.join('\n')}\n`);
|
|
642
|
+
}
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
// The memory section of STATUS.md
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
function memorySection(t, here) {
|
|
647
|
+
const m = t.memories;
|
|
648
|
+
const submissions = m.submissions();
|
|
649
|
+
const merges = m.merges();
|
|
650
|
+
if (submissions.length === 0 && merges.length === 0) {
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
const caseLink = (key) => {
|
|
654
|
+
const id = key.replace(/@r\d+$/, '');
|
|
655
|
+
return link(id, here, feedbackPath(t, { id }));
|
|
656
|
+
};
|
|
657
|
+
const waiting = m.unmerged();
|
|
658
|
+
const out = [
|
|
659
|
+
'## Memory',
|
|
660
|
+
'',
|
|
661
|
+
'No run shares a memory: every try runs on its own private directory. A case whose',
|
|
662
|
+
`with-memory run passes keeps a copy of that memory; every ${t.config.mergeEvery} kept memories are`,
|
|
663
|
+
'merged, with the last merge, into a new numbered graph.',
|
|
664
|
+
'',
|
|
665
|
+
`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(', ')}` : ''}`,
|
|
666
|
+
'',
|
|
667
|
+
];
|
|
668
|
+
if (merges.length > 0) {
|
|
669
|
+
out.push('| Merge | At | Cases in it | New in this merge | Nodes | Edges | Graph | Log |', '| --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
670
|
+
for (const x of [...merges].reverse()) {
|
|
671
|
+
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)} |`);
|
|
672
|
+
}
|
|
673
|
+
out.push('');
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
}
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// STATUS.md - the whole tuning
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
const STEP_CLASS = {
|
|
681
|
+
idle: 'idle',
|
|
682
|
+
run: 'run',
|
|
683
|
+
analyze: 'analyze',
|
|
684
|
+
parked: 'parked',
|
|
685
|
+
};
|
|
686
|
+
// One diagram per worker: the path its case takes through both phases, with
|
|
687
|
+
// the step it is on now filled in.
|
|
688
|
+
function idleWord(t) {
|
|
689
|
+
if (t.improvements.applying) {
|
|
690
|
+
return 'idle - no run starts during an apply';
|
|
691
|
+
}
|
|
692
|
+
return t.drained ? 'idle - no case left to start' : 'idle';
|
|
693
|
+
}
|
|
694
|
+
function workerDiagram(t, w) {
|
|
695
|
+
if (!w.case) {
|
|
696
|
+
return [
|
|
697
|
+
'```mermaid',
|
|
698
|
+
'flowchart LR',
|
|
699
|
+
` w["worker ${w.slot} · ${idleWord(t)} · ${since(Date.now() - w.since)}"]`,
|
|
700
|
+
' classDef idle fill:#6e7781,color:#fff',
|
|
701
|
+
' class w idle',
|
|
702
|
+
'```',
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
const phase = w.phase ?? 'nomem';
|
|
706
|
+
const p = phase === 'mem' ? 'm' : 'n';
|
|
707
|
+
const current = `${p}${w.step === 'analyze' ? 'a' : w.step === 'parked' ? 'p' : 'r'}`;
|
|
708
|
+
const q = t.improvements;
|
|
709
|
+
const tries = phase === 'mem' ? t.config.memTries : t.config.tries;
|
|
710
|
+
const extra = (id) => {
|
|
711
|
+
if (id !== current) {
|
|
712
|
+
return '';
|
|
713
|
+
}
|
|
714
|
+
const now = ` · try ${w.attempt} of ${tries} · ${since(Date.now() - w.since)}`;
|
|
715
|
+
return w.step === 'parked'
|
|
716
|
+
? `${now} · queue ${q.pending.length} of ${t.config.applyAt}${q.applying ? ' · applying' : ''}`
|
|
717
|
+
: now;
|
|
718
|
+
};
|
|
719
|
+
const node = (id, text) => ` ${id}["${text}${extra(id)}"]`;
|
|
720
|
+
const out = [
|
|
721
|
+
'```mermaid',
|
|
722
|
+
'flowchart LR',
|
|
723
|
+
node('nr', 'no memory run'),
|
|
724
|
+
node('na', 'no memory analyze'),
|
|
725
|
+
node('np', 'no memory parked'),
|
|
726
|
+
node('mr', 'with memory run'),
|
|
727
|
+
node('ma', 'with memory analyze'),
|
|
728
|
+
node('mp', 'with memory parked'),
|
|
729
|
+
' cd["completed"]',
|
|
730
|
+
' df["difficult"]',
|
|
731
|
+
' nr --> na',
|
|
732
|
+
' na -->|"passed"| mr',
|
|
733
|
+
' na -->|"failed, tries left"| np',
|
|
734
|
+
' np -->|"applied"| nr',
|
|
735
|
+
' na -->|"no tries left"| df',
|
|
736
|
+
' mr --> ma',
|
|
737
|
+
' ma -->|"cheaper"| cd',
|
|
738
|
+
' ma -->|"failed, tries left"| mp',
|
|
739
|
+
' mp -->|"applied"| mr',
|
|
740
|
+
' ma -->|"no tries left"| df',
|
|
741
|
+
' classDef run fill:#0969da,color:#fff',
|
|
742
|
+
' classDef analyze fill:#8250df,color:#fff',
|
|
743
|
+
' classDef parked fill:#9a6700,color:#fff',
|
|
744
|
+
' classDef past fill:#d0d7de,color:#24292f',
|
|
745
|
+
' classDef done fill:#1a7f37,color:#fff',
|
|
746
|
+
' classDef bad fill:#cf222e,color:#fff',
|
|
747
|
+
' class cd done',
|
|
748
|
+
' class df bad',
|
|
749
|
+
];
|
|
750
|
+
if (phase === 'mem') {
|
|
751
|
+
out.push(' class nr,na,np past');
|
|
752
|
+
}
|
|
753
|
+
out.push(` class ${current} ${STEP_CLASS[w.step]}`, '```');
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
function overviewDiagram(counts) {
|
|
757
|
+
return [
|
|
758
|
+
'```mermaid',
|
|
759
|
+
'flowchart LR',
|
|
760
|
+
` Q["queued · ${counts.queued}"] --> N["no memory · ${counts.nomem}"]`,
|
|
761
|
+
` N --> M["with memory · ${counts.mem}"]`,
|
|
762
|
+
` M --> C["completed · ${counts.completed}"]`,
|
|
763
|
+
` N --> D["difficult · ${counts.difficult}"]`,
|
|
764
|
+
' M --> D',
|
|
765
|
+
` Q -.-> S["stopped · ${counts.stopped}"]`,
|
|
766
|
+
...(counts.failed ? [` N -.-> F["failed · ${counts.failed}"]`, ' M -.-> F'] : []),
|
|
767
|
+
' classDef done fill:#1a7f37,color:#fff',
|
|
768
|
+
' classDef bad fill:#cf222e,color:#fff',
|
|
769
|
+
' classDef live fill:#0969da,color:#fff',
|
|
770
|
+
' classDef broke fill:#9a6700,color:#fff',
|
|
771
|
+
' class C done',
|
|
772
|
+
' class D bad',
|
|
773
|
+
' class N,M live',
|
|
774
|
+
...(counts.failed ? [' class F broke'] : []),
|
|
775
|
+
'```',
|
|
776
|
+
];
|
|
777
|
+
}
|
|
778
|
+
const LANE = {
|
|
779
|
+
idle: 'fill:#f6f8fa,stroke:#d0d7de',
|
|
780
|
+
parked: 'fill:#f6f8fa,stroke:#d0d7de',
|
|
781
|
+
run: 'fill:#ddf4ff,stroke:#0969da,stroke-width:2px',
|
|
782
|
+
analyze: 'fill:#fbefff,stroke:#8250df,stroke-width:2px',
|
|
783
|
+
};
|
|
784
|
+
// A subgraph linked from outside drops its own direction; invisible links keep the slots in order.
|
|
785
|
+
const inOrder = (prefix, n) => n > 1
|
|
786
|
+
? [` ${Array.from({ length: n }, (_, i) => `${prefix}${i + 1}`).join(' ~~~ ')}`]
|
|
787
|
+
: [];
|
|
788
|
+
// A markdown label: real newlines, since some previews swallow `<br/>` before Mermaid sees it.
|
|
789
|
+
const lines = (...parts) => `\`${parts.join('\n')}\``;
|
|
790
|
+
// The whole machine right now: every worker, the queue filling toward an apply, the merge.
|
|
791
|
+
// Each node links to what it is working on.
|
|
792
|
+
function systemDiagram(t, counts) {
|
|
793
|
+
const q = t.improvements;
|
|
794
|
+
const m = t.memories;
|
|
795
|
+
const lit = {
|
|
796
|
+
case: [],
|
|
797
|
+
run: [],
|
|
798
|
+
analyze: [],
|
|
799
|
+
off: [],
|
|
800
|
+
full: [],
|
|
801
|
+
empty: [],
|
|
802
|
+
busy: [],
|
|
803
|
+
kept: [],
|
|
804
|
+
};
|
|
805
|
+
const clicks = [];
|
|
806
|
+
const click = (id, to, tip) => {
|
|
807
|
+
if (to && (to.startsWith('#') || existsSync(to))) {
|
|
808
|
+
const url = to.startsWith('#')
|
|
809
|
+
? to
|
|
810
|
+
: encodeURI(relative(t.dir, to).split(sep).join('/'));
|
|
811
|
+
clicks.push(` click ${id} href "${url}" "${tip}"`);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
const out = [
|
|
815
|
+
'```mermaid',
|
|
816
|
+
'---',
|
|
817
|
+
'config:',
|
|
818
|
+
' flowchart:',
|
|
819
|
+
' wrappingWidth: 480',
|
|
820
|
+
'---',
|
|
821
|
+
'flowchart TB',
|
|
822
|
+
` DS[("dataset · ${counts.queued} queued")] --> D["${lines('dispatcher', 'seats a case on a free worker', 'woken cases first')}"]`,
|
|
823
|
+
` subgraph WORKERS["workers · ${t.busy()} of ${t.workers.length} busy"]`,
|
|
824
|
+
' direction LR',
|
|
825
|
+
];
|
|
826
|
+
click('DS', join(t.root, DATASET_DIR), 'the dataset');
|
|
827
|
+
click('D', join(t.dir, 'events.jsonl'), 'every step, in order');
|
|
828
|
+
const lanes = [];
|
|
829
|
+
for (const w of t.workers) {
|
|
830
|
+
const n = w.slot;
|
|
831
|
+
const step = w.case ? w.step : 'idle';
|
|
832
|
+
let label = idleWord(t);
|
|
833
|
+
if (w.case) {
|
|
834
|
+
const tries = w.phase === 'mem' ? t.config.memTries : t.config.tries;
|
|
835
|
+
label =
|
|
836
|
+
w.attempt === undefined
|
|
837
|
+
? lines(`**${safe(w.case.id, 48)}**`, 'starting')
|
|
838
|
+
: lines(`**${safe(w.case.id, 48)}**`, `${PHASE_WORD[w.phase ?? 'nomem']} · try ${w.attempt} of ${tries} · ${step} ${since(Date.now() - w.since)}`);
|
|
839
|
+
click(`C${n}`, feedbackPath(t, w.case), 'the case: rubric, tries, decisions');
|
|
840
|
+
if (w.attempt !== undefined) {
|
|
841
|
+
const dir = t.attemptDir(w.case, w.phase ?? 'nomem', w.attempt);
|
|
842
|
+
const run = readJson(join(dir, 'run.json'));
|
|
843
|
+
if (run) {
|
|
844
|
+
click(`R${n}`, run.report ?? run.dir, 'the run report');
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
click(`R${n}`, join(dir, 'run.log'), 'the run log, live');
|
|
848
|
+
}
|
|
849
|
+
if (existsSync(join(dir, 'analysis.md'))) {
|
|
850
|
+
click(`A${n}`, join(dir, 'analysis.md'), 'the analysis');
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
click(`A${n}`, join(dir, 'analyze.log'), 'the analysis log, live');
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
out.push(` subgraph W${n}["worker ${n}"]`, ' direction TB', ` C${n}["${label}"] --> R${n}["run"] --> A${n}["analyze"]`, ' end');
|
|
858
|
+
lit[w.case ? 'case' : 'empty'].push(`C${n}`);
|
|
859
|
+
lit[step === 'run' ? 'run' : 'off'].push(`R${n}`);
|
|
860
|
+
lit[step === 'analyze' ? 'analyze' : 'off'].push(`A${n}`);
|
|
861
|
+
lanes.push(` style W${n} ${LANE[step]}`);
|
|
862
|
+
}
|
|
863
|
+
// Edges go to the box, not its nodes, so it keeps LR and the chain puts the lanes in slot order.
|
|
864
|
+
out.push(...inOrder('W', t.workers.length), ' end', ' D --> WORKERS',
|
|
865
|
+
// Not a diamond: an edge to a box ends at the node's bounding box, short of a diamond's sides.
|
|
866
|
+
' WORKERS --> V(["feedback done?"])', ` V -.->|"${lines('passed without memory:', 'again with memory')}"| WORKERS`, ` V -->|"${lines('failed, tries left:', 'park, free the worker')}"| Q`, ` V -->|"no tries left"| X["difficult · ${counts.difficult}"]`, ` V -->|"passed with memory"| CD["completed · ${counts.completed}"]`, ` subgraph Q["improvement queue · ${q.pending.length} of ${t.config.applyAt}"]`, ' direction LR');
|
|
867
|
+
click('X', '#difficult-cases', 'the difficult cases');
|
|
868
|
+
click('CD', '#cases', 'every case');
|
|
869
|
+
const slots = Math.max(t.config.applyAt, q.pending.length);
|
|
870
|
+
for (let i = 0; i < slots; i++) {
|
|
871
|
+
const f = q.pending[i];
|
|
872
|
+
out.push(f
|
|
873
|
+
? ` Q${i + 1}["${lines(`**${safe(f.case, 40)}**`, `${PHASE_WORD[f.phase]} try ${f.attempt} · ${f.improvements.length} improvement(s)`)}"]`
|
|
874
|
+
: ` Q${i + 1}["empty"]`);
|
|
875
|
+
lit[f ? 'full' : 'empty'].push(`Q${i + 1}`);
|
|
876
|
+
if (f) {
|
|
877
|
+
const dir = t.attemptDir({ id: f.case, rev: f.caseRev }, f.phase, f.attempt);
|
|
878
|
+
click(`Q${i + 1}`, join(dir, 'analysis.md'), 'the analysis asking for these');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
out.push(...inOrder('Q', slots));
|
|
882
|
+
const last = q.applies().at(-1);
|
|
883
|
+
const applier = !q.applying
|
|
884
|
+
? `idle${last ? ` · last ${last.name}` : ''}`
|
|
885
|
+
: q.inApply.length > 0
|
|
886
|
+
? `applying ${q.inApply.length} request(s) · no run starts`
|
|
887
|
+
: 'waiting for runs · no run starts';
|
|
888
|
+
out.push(' end',
|
|
889
|
+
// AP's own nodes link outside, so Mermaid cannot end an edge on the AP box itself.
|
|
890
|
+
' Q -->|"full, or nothing left to start"| P1', ` subgraph AP["applier · ${applier}"]`, ` P1["wait for runs in flight · ${q.runs}"] --> P2["${lines('meta run /finetune-apply', 'then zen check')}"]`, ` P2 --> P3["${lines('applied.json', 'wake the batch')}"]`, ' end', ` P2 -->|"writes the next version"| S[("${lines(`system prose · v${t.system.version()}`, 'read by every new run')}")]`, ' P3 -->|"wake"| D');
|
|
891
|
+
const active = !q.applying ? undefined : q.runs > 0 ? 'P1' : 'P2';
|
|
892
|
+
for (const p of ['P1', 'P2', 'P3']) {
|
|
893
|
+
lit[p === active ? 'busy' : 'off'].push(p);
|
|
894
|
+
}
|
|
895
|
+
if (last) {
|
|
896
|
+
click('P2', applyPath(last.dir), q.applying ? 'this apply' : 'the last apply');
|
|
897
|
+
click('P3', join(last.dir, 'diff.patch'), 'what the last apply changed');
|
|
898
|
+
}
|
|
899
|
+
click('S', t.system.snapshotDir(t.system.version()), 'this version of the prose');
|
|
900
|
+
const waiting = m.unmerged();
|
|
901
|
+
const kept = Math.max(t.config.mergeEvery, waiting.length);
|
|
902
|
+
out.push(' CD -->|"keep its memory"| M', ` subgraph M["memory to merge · ${waiting.length} of ${t.config.mergeEvery}"]`, ' direction LR');
|
|
903
|
+
for (let i = 0; i < kept; i++) {
|
|
904
|
+
const s = waiting[i];
|
|
905
|
+
out.push(` K${i + 1}["${s ? safe(s.case, 40) : 'empty'}"]`);
|
|
906
|
+
lit[s ? 'kept' : 'empty'].push(`K${i + 1}`);
|
|
907
|
+
if (s) {
|
|
908
|
+
click(`K${i + 1}`, s.dir, 'the kept memory');
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const merged = m.merges().filter((x) => x.ok);
|
|
912
|
+
click('MG', merged.at(-1)?.dir, 'the latest merged graph');
|
|
913
|
+
out.push(...inOrder('K', kept), ' end', ` M -->|"full, or tuning ends"| MG[("merged graphs · ${merged.length}")]`, ...clicks, ' classDef case fill:#ffffff,stroke:#57606a,color:#24292f', ' classDef run fill:#0969da,stroke:#0550ae,color:#ffffff', ' classDef analyze fill:#8250df,stroke:#6639ba,color:#ffffff', ' classDef off fill:#ffffff,stroke:#d0d7de,color:#8c959f', ' classDef full fill:#d4a72c,stroke:#9a6700,color:#ffffff', ' classDef empty fill:#ffffff,stroke:#d0d7de,stroke-dasharray:4 3,color:#8c959f', ' classDef busy fill:#e16f24,stroke:#bc4c00,color:#ffffff', ' classDef kept fill:#1b7c83,stroke:#136061,color:#ffffff', ' classDef done fill:#1a7f37,stroke:#116329,color:#ffffff', ' classDef bad fill:#cf222e,stroke:#a40e26,color:#ffffff', ' classDef store fill:#ddf4ff,stroke:#54aeff,color:#0a3069', ' classDef hub fill:#eaeef2,stroke:#57606a,color:#24292f', ...Object.entries(lit)
|
|
914
|
+
.filter(([, ids]) => ids.length > 0)
|
|
915
|
+
.map(([cls, ids]) => ` class ${ids.join(',')} ${cls}`), ' class CD done', ' class X bad', ' class DS,S,MG store', ' class D,V hub', ' style WORKERS fill:#ffffff,stroke:#8c959f', ...lanes, ` style Q fill:#fff8c5,stroke:#d4a72c${q.pending.length >= t.config.applyAt ? ',stroke-width:3px' : ''}`, ` style AP ${q.applying ? 'fill:#fff1e5,stroke:#e16f24,stroke-width:3px' : 'fill:#f6f8fa,stroke:#d0d7de'}`, ' style M fill:#e6f6f5,stroke:#1b7c83', '```');
|
|
916
|
+
const running = lit.run.length;
|
|
917
|
+
const analyzing = lit.analyze.length;
|
|
918
|
+
const now = [
|
|
919
|
+
running > 0 && `- **Blue** - ${running} worker(s) running a case`,
|
|
920
|
+
analyzing > 0 && `- **Purple** - ${analyzing} worker(s) analyzing the run just finished`,
|
|
921
|
+
q.applying &&
|
|
922
|
+
'- **Orange** - fixes are being applied to the agent instructions; no new run starts until that is done',
|
|
923
|
+
q.pending.length > 0 &&
|
|
924
|
+
`- **Yellow** - ${q.pending.length} failed case(s) waiting for fixes; they are applied when ${t.config.applyAt} wait, or when no other case can run`,
|
|
925
|
+
waiting.length > 0 &&
|
|
926
|
+
`- **Teal** - memory from ${waiting.length} passed case(s) waiting to be merged; merged when ${t.config.mergeEvery} wait`,
|
|
927
|
+
].filter((x) => typeof x === 'string');
|
|
928
|
+
return [
|
|
929
|
+
...(now.length > 0 ? ['Now:', '', ...now] : ['Nothing is in progress right now.']),
|
|
930
|
+
'',
|
|
931
|
+
'Click a box to open it.',
|
|
932
|
+
'',
|
|
933
|
+
...out,
|
|
934
|
+
];
|
|
935
|
+
}
|
|
936
|
+
function summarySection(spent, tuning, memory) {
|
|
937
|
+
const phases = ['nomem', 'mem'].filter((p) => spent[p].runs > 0);
|
|
938
|
+
if (phases.length === 0) {
|
|
939
|
+
return [];
|
|
940
|
+
}
|
|
941
|
+
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)} |`;
|
|
942
|
+
const out = [
|
|
943
|
+
'## Summary',
|
|
944
|
+
'',
|
|
945
|
+
`Trend compares totals over the same cases; within ${SAME}% is \`same\`. Run time is the wall clock of \`zen run\`.`,
|
|
946
|
+
'',
|
|
947
|
+
'| Phase | Runs | Run time | LLM calls | Tool calls | Input | Cached | Output | Per run: tokens / time |',
|
|
948
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
949
|
+
...phases.map((p) => row(PHASE_WORD[p], spent[p].runs, spent[p].f)),
|
|
950
|
+
...(phases.length > 1
|
|
951
|
+
? [
|
|
952
|
+
row('**total**', spent.nomem.runs + spent.mem.runs, addFlat(spent.nomem.f, spent.mem.f)),
|
|
953
|
+
]
|
|
954
|
+
: []),
|
|
955
|
+
'',
|
|
956
|
+
];
|
|
957
|
+
if (tuning.passed > 0) {
|
|
958
|
+
const fixed = tuning.passed - tuning.first;
|
|
959
|
+
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
|
|
960
|
+
? compareTable('First try', 'Passing try', tuning.before, tuning.after)
|
|
961
|
+
: []));
|
|
962
|
+
}
|
|
963
|
+
if (memory.cases > 0) {
|
|
964
|
+
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));
|
|
965
|
+
}
|
|
966
|
+
return out;
|
|
967
|
+
}
|
|
968
|
+
export function writeStatus(t) {
|
|
969
|
+
const here = t.dir;
|
|
970
|
+
const cases = selection(t);
|
|
971
|
+
const store = DatasetStore.open(t.root);
|
|
972
|
+
const counts = {
|
|
973
|
+
queued: 0,
|
|
974
|
+
nomem: 0,
|
|
975
|
+
mem: 0,
|
|
976
|
+
completed: 0,
|
|
977
|
+
difficult: 0,
|
|
978
|
+
failed: 0,
|
|
979
|
+
stopped: 0,
|
|
980
|
+
};
|
|
981
|
+
const rows = [];
|
|
982
|
+
const difficult = [];
|
|
983
|
+
const failed = [];
|
|
984
|
+
let everything = {};
|
|
985
|
+
const spent = {
|
|
986
|
+
nomem: { runs: 0, f: blank() },
|
|
987
|
+
mem: { runs: 0, f: blank() },
|
|
988
|
+
};
|
|
989
|
+
const tuning = { passed: 0, first: 0, before: blank(), after: blank() };
|
|
990
|
+
const memory = { cases: 0, cheaper: 0, before: blank(), after: blank() };
|
|
991
|
+
for (const c of cases) {
|
|
992
|
+
const w = t.workers.find((x) => x.case?.id === c.id);
|
|
993
|
+
const parked = t.improvements.pending.find((f) => f.case === c.id) ??
|
|
994
|
+
t.improvements.inApply.find((f) => f.case === c.id);
|
|
995
|
+
const r = resultOf(t, c);
|
|
996
|
+
const attempts = attemptsOf(t, c);
|
|
997
|
+
if (r) {
|
|
998
|
+
counts[r.state]++;
|
|
999
|
+
}
|
|
1000
|
+
else if (w) {
|
|
1001
|
+
counts[w.phase ?? 'nomem']++;
|
|
1002
|
+
}
|
|
1003
|
+
else if (parked) {
|
|
1004
|
+
counts[parked.phase]++;
|
|
1005
|
+
}
|
|
1006
|
+
else if (attempts.length > 0 && !t.claimed.has(c.id)) {
|
|
1007
|
+
counts.stopped++;
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
counts.queued++;
|
|
1011
|
+
}
|
|
1012
|
+
const nomem = attempts.filter((a) => a.phase === 'nomem');
|
|
1013
|
+
const mem = attempts.filter((a) => a.phase === 'mem');
|
|
1014
|
+
const last = [...attempts].reverse().find((a) => a.feedback);
|
|
1015
|
+
const calls = nomem.map((a) => a.run?.turns).filter((n) => n !== undefined);
|
|
1016
|
+
const memCalls = mem.map((a) => a.run?.turns).filter((n) => n !== undefined);
|
|
1017
|
+
const caseBy = caseTokens(t, c);
|
|
1018
|
+
everything = mergeStages(everything, caseBy);
|
|
1019
|
+
for (const a of attempts) {
|
|
1020
|
+
const f = runFlat(a.run);
|
|
1021
|
+
if (f) {
|
|
1022
|
+
spent[a.phase].runs++;
|
|
1023
|
+
spent[a.phase].f = addFlat(spent[a.phase].f, f);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const passed = passedWithout(attempts);
|
|
1027
|
+
const firstFlat = runFlat(nomem[0]?.run);
|
|
1028
|
+
const passedFlat = runFlat(passed?.run);
|
|
1029
|
+
if (passed) {
|
|
1030
|
+
tuning.passed++;
|
|
1031
|
+
}
|
|
1032
|
+
if (passed?.attempt === 1) {
|
|
1033
|
+
tuning.first++;
|
|
1034
|
+
}
|
|
1035
|
+
else if (firstFlat && passedFlat) {
|
|
1036
|
+
tuning.before = addFlat(tuning.before, firstFlat);
|
|
1037
|
+
tuning.after = addFlat(tuning.after, passedFlat);
|
|
1038
|
+
}
|
|
1039
|
+
const withMem = runFlat(r?.state === 'completed'
|
|
1040
|
+
? mem.find((a) => a.feedback?.done)?.run
|
|
1041
|
+
: latestWith(attempts)?.run);
|
|
1042
|
+
if (r?.state === 'completed' && withMem && passedFlat) {
|
|
1043
|
+
memory.cases++;
|
|
1044
|
+
memory.cheaper += tokensIn(withMem) < tokensIn(passedFlat) ? 1 : 0;
|
|
1045
|
+
memory.before = addFlat(memory.before, passedFlat);
|
|
1046
|
+
memory.after = addFlat(memory.after, withMem);
|
|
1047
|
+
}
|
|
1048
|
+
const effect = withMem && passedFlat
|
|
1049
|
+
? `${change('tokens', tokensIn(withMem), tokensIn(passedFlat))} tokens · ${change('time', withMem.wallMs, passedFlat.wallMs)} time${r?.state === 'completed' ? '' : ' (so far)'}`
|
|
1050
|
+
: '-';
|
|
1051
|
+
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
|
+
if (r?.state === 'difficult') {
|
|
1053
|
+
difficult.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${PHASE_WORD[r.phase]} | ${cell(r.reason)} | ${r.phase === 'nomem' ? nomem.length : mem.length} | ${cell(last?.feedback?.summary)} | ${last && existsSync(join(last.dir, 'analysis.md')) ? link('last analysis', here, join(last.dir, 'analysis.md')) : '-'} |`);
|
|
1054
|
+
}
|
|
1055
|
+
if (r?.state === 'failed') {
|
|
1056
|
+
failed.push(`| ${link(c.id, here, feedbackPath(t, c))} | ${PHASE_WORD[r.phase]}, try ${(r.phase === 'nomem' ? nomem : mem).length || 1} | ${cell(r.reason)} | ${r.at.slice(11, 16)} |`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const done = counts.completed + counts.difficult;
|
|
1060
|
+
const bar = cases.length ? Math.round((done / cases.length) * 20) : 0;
|
|
1061
|
+
const state = t.finished ? 'finished' : t.stopping ? 'stopping' : 'running';
|
|
1062
|
+
const q = t.improvements;
|
|
1063
|
+
const out = [
|
|
1064
|
+
`# Fine-tuning - ${basename(t.root)}`,
|
|
1065
|
+
'',
|
|
1066
|
+
`**${state}** · started ${new Date(t.startedAt).toISOString().slice(0, 16).replace('T', ' ')} · elapsed ${since(Date.now() - t.startedAt)} · updated ${new Date().toISOString().slice(11, 19)}`,
|
|
1067
|
+
`system **v${t.system.version()}** (applies: ${q.applies().length}) · dataset rev **${store.manifest.revision}** · workers **${t.workers.filter((w) => w.case).length} / ${t.workers.length}** · apply at **${t.config.applyAt}** · tries ${t.config.tries} without memory, ${t.config.memTries} with`,
|
|
1068
|
+
'',
|
|
1069
|
+
`\`${'█'.repeat(bar)}${'░'.repeat(20 - bar)}\` ${done} of ${cases.length} cases done - ${counts.completed} completed, ${counts.difficult} difficult${counts.failed ? ` · ${counts.failed} failed, they resume at the next start` : ''}`,
|
|
1070
|
+
'',
|
|
1071
|
+
...summarySection(spent, tuning, memory),
|
|
1072
|
+
'## Overview',
|
|
1073
|
+
'',
|
|
1074
|
+
...overviewDiagram(counts),
|
|
1075
|
+
'',
|
|
1076
|
+
'## System',
|
|
1077
|
+
'',
|
|
1078
|
+
...systemDiagram(t, counts),
|
|
1079
|
+
'',
|
|
1080
|
+
'## Workers',
|
|
1081
|
+
'',
|
|
1082
|
+
...t.workers.flatMap((w) => [
|
|
1083
|
+
`### Worker ${w.slot}${w.case ? ` - ${link(w.case.id, here, feedbackPath(t, w.case))}` : ''}`,
|
|
1084
|
+
'',
|
|
1085
|
+
...workerDiagram(t, w),
|
|
1086
|
+
'',
|
|
1087
|
+
...(w.case ? tokenTable(caseTokens(t, w.case)) : []),
|
|
1088
|
+
]),
|
|
1089
|
+
'| Worker | Case | Class | Phase | Try | Step | Since | Run | Analysis | Session |',
|
|
1090
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
1091
|
+
];
|
|
1092
|
+
for (const w of t.workers) {
|
|
1093
|
+
if (!w.case) {
|
|
1094
|
+
out.push(`| ${w.slot} | - | - | - | - | ${idleWord(t)} | ${since(Date.now() - w.since)} | - | - | - |`);
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
const dir = t.attemptDir(w.case, w.phase ?? 'nomem', w.attempt ?? 1);
|
|
1098
|
+
const run = readJson(join(dir, 'run.json'));
|
|
1099
|
+
const session = existsSync(join(t.caseDir(w.case), 'session'))
|
|
1100
|
+
? readFileSync(join(t.caseDir(w.case), 'session'), 'utf8').trim()
|
|
1101
|
+
: undefined;
|
|
1102
|
+
out.push(`| ${w.slot} | ${link(w.case.id, here, feedbackPath(t, w.case))} | ${cell(w.case.class)} | ${PHASE_WORD[w.phase ?? 'nomem']} | ${w.attempt} / ${w.phase === 'mem' ? t.config.memTries : t.config.tries} | ${w.step} | ${since(Date.now() - w.since)} | ${run ? `${link('run', here, run.dir)}${run.report ? ` · ${link('report', here, run.report)}` : ''}` : '-'} | ${existsSync(join(dir, 'analysis.md')) ? link('analysis', here, join(dir, 'analysis.md')) : '-'} | ${session ? `\`zen meta resume ${basename(t.root)} ${session}\`` : '-'} |`);
|
|
1103
|
+
}
|
|
1104
|
+
out.push('', `## Improvement queue - ${q.pending.length} of ${t.config.applyAt} parked`, '');
|
|
1105
|
+
out.push(q.applying
|
|
1106
|
+
? 'Applying now: no new run starts until it is done.'
|
|
1107
|
+
: `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
|
+
if (q.pending.length > 0) {
|
|
1109
|
+
out.push('| Request | Case | Phase | Try | Improvements | Summary |', '| --- | --- | --- | --- | --- | --- |');
|
|
1110
|
+
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' : '-')} |`);
|
|
1140
|
+
}
|
|
1141
|
+
out.push('');
|
|
1142
|
+
}
|
|
1143
|
+
const allStages = mergeStages(everything, {
|
|
1144
|
+
apply: mergeModels(...applies.map((a) => envelopesIn(a.dir, 'apply'))),
|
|
1145
|
+
});
|
|
1146
|
+
t.progress = { counts, total: cases.length, spent: allStages };
|
|
1147
|
+
const tokenRows = tokenTable(allStages);
|
|
1148
|
+
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, '');
|
|
1158
|
+
}
|
|
1159
|
+
out.push('## Recent events', '');
|
|
1160
|
+
for (const e of t.recent.slice(0, 20)) {
|
|
1161
|
+
const who = [
|
|
1162
|
+
e.worker ? `worker ${e.worker}` : '',
|
|
1163
|
+
e.case ?? '',
|
|
1164
|
+
e.phase ? `${PHASE_WORD[e.phase]}${e.attempt ? ` try ${e.attempt}` : ''}` : '',
|
|
1165
|
+
].filter(Boolean);
|
|
1166
|
+
out.push(`- ${e.at.slice(11, 19)} · ${[...who, e.what].join(' · ')}${e.detail ? ` - ${cell(e.detail)}` : ''}`);
|
|
1167
|
+
}
|
|
1168
|
+
out.push(`- … everything in ${link('events.jsonl', here, join(t.dir, 'events.jsonl'))}`, '', '## Files', '', '- `cases/<id>/FEEDBACK.md` - one case: rubric, latest analysis, decisions, history', '- `cases/<id>/r<rev>/<NN>-<phase>/` - one try: `request.json`, `run.json`, `memory/`, `workspace/`, `feedback.json`, `analysis.md`, logs', '- `applies/<NNN>/APPLY.md` - one apply: requests, decisions, files changed with their diffs, zen check', '- `applies/<NNN>/` - `inputs.json`, `apply.log`, `check.log`, `changes.md`, `decisions.json`, `diff.patch`', '- `memories/` - `submissions.jsonl`, `merges.jsonl`, `submitted/<case>@r<rev>/`, `merged/mNN/`', '- `systems/` - `systems.jsonl` and a copy of `agents.yaml` + `agents/` per version', '- `events.jsonl` - every step, in order; `loop.json` - the settings', '- `journal.jsonl` - steps begun and not yet closed; a restart rolls back what a kill cut off', '- `STATUS.html`, `cases/<id>/FEEDBACK.html` - the same pages, rendered for a browser', '');
|
|
1169
|
+
const md = `${out.join('\n')}\n`;
|
|
1170
|
+
atomic(join(t.dir, 'STATUS.md'), md);
|
|
1171
|
+
atomic(join(t.dir, 'STATUS.html'), markdownPage(`Fine-tuning - ${basename(t.root)}`, md, !t.finished));
|
|
1172
|
+
}
|
|
1173
|
+
// ---------------------------------------------------------------------------
|
|
1174
|
+
// STATUS.html, FEEDBACK.html - the same Markdown, rendered in the browser
|
|
1175
|
+
// ---------------------------------------------------------------------------
|
|
1176
|
+
const MARKED_URL = 'https://cdn.jsdelivr.net/npm/marked@14/lib/marked.esm.js';
|
|
1177
|
+
const PURIFY_URL = 'https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.es.mjs';
|
|
1178
|
+
// As in neo's inspect page: no byte of the payload can close the script element.
|
|
1179
|
+
const embed = (v) => JSON.stringify(v)
|
|
1180
|
+
.replace(/</g, '\\u003c')
|
|
1181
|
+
.replace(/>/g, '\\u003e')
|
|
1182
|
+
.replace(/&/g, '\\u0026')
|
|
1183
|
+
.replace(/\u2028/g, '\\u2028')
|
|
1184
|
+
.replace(/\u2029/g, '\\u2029');
|
|
1185
|
+
export function markdownPage(title, md, live) {
|
|
1186
|
+
return `<!doctype html>
|
|
1187
|
+
<html lang="en">
|
|
1188
|
+
<head>
|
|
1189
|
+
<meta charset="utf-8">
|
|
1190
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1191
|
+
${live ? '<meta http-equiv="refresh" content="10">\n' : ''}<title>${title.replace(/[&<>"]/g, (ch) => `&#${ch.charCodeAt(0)};`)}</title>
|
|
1192
|
+
<style>
|
|
1193
|
+
body{font:14px/1.5 system-ui,sans-serif;margin:0;color:#24292f}
|
|
1194
|
+
main{margin-left:240px;padding:1em 2em;max-width:1600px}
|
|
1195
|
+
#toc{position:fixed;top:0;left:0;bottom:0;width:220px;overflow:auto;padding:1em 6px;box-sizing:border-box;border-right:1px solid #d0d7de;background:#f6f8fa;font-size:13px}
|
|
1196
|
+
#toc a{display:block;padding:2px 8px;border-radius:4px;color:#24292f;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1197
|
+
#toc a.top{font-weight:600;margin-bottom:6px}
|
|
1198
|
+
#toc a.sub{padding-left:22px;color:#57606a}
|
|
1199
|
+
#toc a:hover{background:#eaeef2}
|
|
1200
|
+
#toc a.on{background:#ddf4ff;color:#0969da;font-weight:600}
|
|
1201
|
+
h1,h2,h3{scroll-margin-top:8px}
|
|
1202
|
+
@media (max-width:800px){#toc{display:none}main{margin-left:0}}
|
|
1203
|
+
table{border-collapse:collapse;margin:1em 0}th,td{border:1px solid #d0d7de;padding:4px 8px;vertical-align:top}
|
|
1204
|
+
th{background:#f6f8fa}code{background:#f6f8fa;padding:0 4px;border-radius:4px}
|
|
1205
|
+
pre{background:#f6f8fa;padding:8px;overflow:auto}pre.mermaid{background:none}
|
|
1206
|
+
a{color:#0969da}
|
|
1207
|
+
</style>
|
|
1208
|
+
</head>
|
|
1209
|
+
<body>
|
|
1210
|
+
<nav id="toc"></nav>
|
|
1211
|
+
<main id="doc">loading…</main>
|
|
1212
|
+
<script id="md" type="application/json">${embed(md)}</script>
|
|
1213
|
+
<script type="module">
|
|
1214
|
+
const md = JSON.parse(document.getElementById('md').textContent);
|
|
1215
|
+
const [{ marked }, { default: DOMPurify }, { default: mermaid }] = await Promise.all([
|
|
1216
|
+
import(${embed(MARKED_URL)}),
|
|
1217
|
+
import(${embed(PURIFY_URL)}),
|
|
1218
|
+
import(${embed(MERMAID_URL)}),
|
|
1219
|
+
]);
|
|
1220
|
+
const doc = document.getElementById('doc');
|
|
1221
|
+
doc.innerHTML = DOMPurify.sanitize(marked.parse(md));
|
|
1222
|
+
// GitHub's slugs, so the diagrams' #anchors land as they do in the .md.
|
|
1223
|
+
for (const h of doc.querySelectorAll('h1, h2, h3, h4')) {
|
|
1224
|
+
h.id = h.textContent.trim().toLowerCase().replace(/[^\\w\\s-]/g, '').replace(/\\s/g, '-');
|
|
1225
|
+
}
|
|
1226
|
+
const toc = document.getElementById('toc');
|
|
1227
|
+
const heads = [...doc.querySelectorAll('h1, h2, h3')];
|
|
1228
|
+
for (const h of heads) {
|
|
1229
|
+
const a = document.createElement('a');
|
|
1230
|
+
a.href = '#' + h.id;
|
|
1231
|
+
a.textContent = h.textContent;
|
|
1232
|
+
a.title = h.textContent;
|
|
1233
|
+
a.className = h.tagName === 'H1' ? 'top' : h.tagName === 'H3' ? 'sub' : '';
|
|
1234
|
+
toc.append(a);
|
|
1235
|
+
}
|
|
1236
|
+
for (const code of doc.querySelectorAll('code.language-mermaid')) {
|
|
1237
|
+
const pre = document.createElement('pre');
|
|
1238
|
+
pre.className = 'mermaid';
|
|
1239
|
+
pre.textContent = code.textContent;
|
|
1240
|
+
code.parentElement.replaceWith(pre);
|
|
1241
|
+
}
|
|
1242
|
+
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });
|
|
1243
|
+
await mermaid.run({ querySelector: 'pre.mermaid' });
|
|
1244
|
+
// A page with an .html twin links to it; any other .md opens as the file it is.
|
|
1245
|
+
for (const a of doc.querySelectorAll('a')) {
|
|
1246
|
+
for (const at of ['href', 'xlink:href']) {
|
|
1247
|
+
const v = a.getAttribute(at);
|
|
1248
|
+
if (v) {
|
|
1249
|
+
a.setAttribute(at, v.replace(/(^|\\/)(STATUS|FEEDBACK)\\.md(?=#|$)/, '$1$2.html'));
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
// The live refresh would otherwise drop the reader back at the top every 10 s.
|
|
1254
|
+
const key = 'scroll:' + location.pathname;
|
|
1255
|
+
history.scrollRestoration = 'manual';
|
|
1256
|
+
addEventListener('beforeunload', () => sessionStorage.setItem(key, String(scrollY)));
|
|
1257
|
+
const kept = sessionStorage.getItem(key);
|
|
1258
|
+
if (kept !== null) {
|
|
1259
|
+
scrollTo(0, Number(kept));
|
|
1260
|
+
} else if (location.hash) {
|
|
1261
|
+
document.getElementById(decodeURIComponent(location.hash.slice(1)))?.scrollIntoView();
|
|
1262
|
+
}
|
|
1263
|
+
const mark = () => {
|
|
1264
|
+
let current = heads[0];
|
|
1265
|
+
for (const h of heads) {
|
|
1266
|
+
if (h.getBoundingClientRect().top > 80) break;
|
|
1267
|
+
current = h;
|
|
1268
|
+
}
|
|
1269
|
+
for (const a of toc.children) {
|
|
1270
|
+
a.classList.toggle('on', a.hash === '#' + current?.id);
|
|
1271
|
+
}
|
|
1272
|
+
const on = toc.querySelector('a.on');
|
|
1273
|
+
if (on && (on.offsetTop < toc.scrollTop || on.offsetTop + on.offsetHeight > toc.scrollTop + toc.clientHeight)) {
|
|
1274
|
+
toc.scrollTop = on.offsetTop - toc.clientHeight / 2;
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
addEventListener('scroll', mark, { passive: true });
|
|
1278
|
+
mark();
|
|
1279
|
+
</script>
|
|
1280
|
+
</body>
|
|
1281
|
+
</html>
|
|
1282
|
+
`;
|
|
1283
|
+
}
|
|
1284
|
+
//# sourceMappingURL=report.js.map
|