@zenera/cli 1.1.24 → 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.
Files changed (167) hide show
  1. package/README.md +53 -0
  2. package/dist/ask.js +28 -5
  3. package/dist/banner.js +2 -2
  4. package/dist/batch.d.ts +6 -0
  5. package/dist/batch.js +76 -1
  6. package/dist/catalog.d.ts +11 -1
  7. package/dist/catalog.js +80 -7
  8. package/dist/commands/inspect.d.ts +47 -1
  9. package/dist/commands/inspect.js +167 -18
  10. package/dist/commands/memory.js +128 -29
  11. package/dist/commands/meta.js +311 -21
  12. package/dist/commands/models.js +96 -12
  13. package/dist/commands/open.js +4 -4
  14. package/dist/commands/run.js +38 -4
  15. package/dist/commands/sandbox.js +76 -14
  16. package/dist/engine.d.ts +5 -0
  17. package/dist/engine.js +1 -0
  18. package/dist/eventlog.d.ts +59 -0
  19. package/dist/eventlog.js +146 -0
  20. package/dist/liveness.d.ts +7 -0
  21. package/dist/liveness.js +15 -1
  22. package/dist/meta/editor.d.ts +9 -0
  23. package/dist/meta/editor.js +60 -0
  24. package/dist/meta/finetune/children.d.ts +8 -0
  25. package/dist/meta/finetune/children.js +76 -0
  26. package/dist/meta/finetune/command.d.ts +30 -0
  27. package/dist/meta/finetune/command.js +338 -0
  28. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  29. package/dist/meta/finetune/dataset/anchors.js +239 -0
  30. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  31. package/dist/meta/finetune/dataset/changes.js +319 -0
  32. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  33. package/dist/meta/finetune/dataset/command.js +1011 -0
  34. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  35. package/dist/meta/finetune/dataset/drift.js +71 -0
  36. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  37. package/dist/meta/finetune/dataset/export.js +51 -0
  38. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  39. package/dist/meta/finetune/dataset/index.js +2 -0
  40. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  41. package/dist/meta/finetune/dataset/sample.js +197 -0
  42. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  43. package/dist/meta/finetune/dataset/schema.js +216 -0
  44. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  45. package/dist/meta/finetune/dataset/store.js +177 -0
  46. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  47. package/dist/meta/finetune/dataset/types.js +39 -0
  48. package/dist/meta/finetune/failure.d.ts +15 -0
  49. package/dist/meta/finetune/failure.js +26 -0
  50. package/dist/meta/finetune/feedback.d.ts +45 -0
  51. package/dist/meta/finetune/feedback.js +85 -0
  52. package/dist/meta/finetune/improvements.d.ts +51 -0
  53. package/dist/meta/finetune/improvements.js +244 -0
  54. package/dist/meta/finetune/index.d.ts +2 -0
  55. package/dist/meta/finetune/index.js +2 -0
  56. package/dist/meta/finetune/journal.d.ts +35 -0
  57. package/dist/meta/finetune/journal.js +135 -0
  58. package/dist/meta/finetune/live.d.ts +74 -0
  59. package/dist/meta/finetune/live.js +226 -0
  60. package/dist/meta/finetune/loop.d.ts +3 -0
  61. package/dist/meta/finetune/loop.js +114 -0
  62. package/dist/meta/finetune/memories.d.ts +47 -0
  63. package/dist/meta/finetune/memories.js +154 -0
  64. package/dist/meta/finetune/report.d.ts +33 -0
  65. package/dist/meta/finetune/report.js +1284 -0
  66. package/dist/meta/finetune/retry.d.ts +24 -0
  67. package/dist/meta/finetune/retry.js +96 -0
  68. package/dist/meta/finetune/sampler.d.ts +18 -0
  69. package/dist/meta/finetune/sampler.js +41 -0
  70. package/dist/meta/finetune/system.d.ts +27 -0
  71. package/dist/meta/finetune/system.js +132 -0
  72. package/dist/meta/finetune/train.d.ts +39 -0
  73. package/dist/meta/finetune/train.js +391 -0
  74. package/dist/meta/finetune/tui.d.ts +16 -0
  75. package/dist/meta/finetune/tui.js +291 -0
  76. package/dist/meta/finetune/tuning.d.ts +167 -0
  77. package/dist/meta/finetune/tuning.js +289 -0
  78. package/dist/meta/finetune/usage.d.ts +62 -0
  79. package/dist/meta/finetune/usage.js +225 -0
  80. package/dist/meta/host.d.ts +14 -0
  81. package/dist/meta/host.js +20 -0
  82. package/dist/meta/index.d.ts +11 -0
  83. package/dist/meta/index.js +11 -0
  84. package/dist/meta/launch.d.ts +81 -0
  85. package/dist/meta/launch.js +286 -0
  86. package/dist/meta/model.d.ts +44 -0
  87. package/dist/meta/model.js +119 -0
  88. package/dist/meta/prompts.d.ts +18 -0
  89. package/dist/meta/prompts.js +61 -0
  90. package/dist/meta/relay.d.ts +20 -0
  91. package/dist/meta/relay.js +123 -0
  92. package/dist/meta/tokens.d.ts +52 -0
  93. package/dist/meta/tokens.js +226 -0
  94. package/dist/meta/window.d.ts +61 -0
  95. package/dist/meta/window.js +198 -0
  96. package/dist/meta/wire.d.ts +42 -0
  97. package/dist/meta/wire.js +129 -0
  98. package/dist/modelref.d.ts +16 -0
  99. package/dist/modelref.js +41 -0
  100. package/dist/narrate.js +5 -2
  101. package/dist/podman.d.ts +26 -0
  102. package/dist/podman.js +83 -3
  103. package/dist/projects.d.ts +0 -5
  104. package/dist/projects.js +2 -15
  105. package/dist/sandbox.d.ts +2 -0
  106. package/dist/sandbox.js +10 -3
  107. package/dist/scaffold.d.ts +15 -6
  108. package/dist/scaffold.js +5 -60
  109. package/dist/session.js +4 -29
  110. package/dist/term.d.ts +2 -0
  111. package/dist/term.js +11 -2
  112. package/dist/trace.js +53 -2
  113. package/dist/tui/models.d.ts +33 -0
  114. package/dist/tui/models.js +389 -0
  115. package/dist/tui/wrap.d.ts +5 -0
  116. package/dist/tui/wrap.js +12 -0
  117. package/dist/usage.d.ts +78 -0
  118. package/dist/usage.js +51 -0
  119. package/package.json +3 -2
  120. package/templates/editor/.github/copilot-instructions.md +31 -102
  121. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  122. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  123. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  124. package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
  125. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  126. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  127. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  128. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  129. package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
  130. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  131. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  132. package/templates/editor/.github/skills/zen-cli/references/inspect.md +47 -9
  133. package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
  134. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  135. package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
  136. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  137. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  138. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  139. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  140. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  141. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1449 -658
  142. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  143. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  144. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  145. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  146. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  147. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  148. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  149. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  150. package/templates/editor/.github/skills/zen-inspect/SKILL.md +60 -25
  151. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  152. package/templates/editor/.github/skills/zen-memory/SKILL.md +18 -14
  153. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  154. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  155. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  156. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +7 -3
  157. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
  158. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  159. package/templates/editor/.vscode/settings.json +1 -0
  160. package/templates/project/agents/files-instructions.md +29 -20
  161. package/templates/project/agents/memory-instructions.md +31 -32
  162. package/templates/project/agents/tools-instructions.md +7 -4
  163. package/templates/project/gitignore +2 -0
  164. package/dist/meta.d.ts +0 -221
  165. package/dist/meta.js +0 -684
  166. package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
  167. package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
@@ -0,0 +1,797 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ //
4
+ // Report on one run's batch: what each case did, and where its trajectory is.
5
+ //
6
+ // `index`, `graphs` and `compare` each open by saying which memory the run had -
7
+ // NO MEMORY, WITH MEMORY or MEMORY OFF - because the same trajectory means
8
+ // opposite things either way.
9
+ //
10
+ // Run `oom` first, and do not expect `failures` to have caught it: a case whose
11
+ // command was killed usually recovers, answers anyway and is recorded `ok`. A
12
+ // run can read "16 items, 16 ok, 0 failed" with six of them OOM-killed inside,
13
+ // and then nothing it did is evidence about the prompt. See the
14
+ // zen-finetune SKILL.md: the fix is half the concurrency, never below 4.
15
+ //
16
+ // `graphs` saves the most: every case's graph.mmd with its id, verdict and
17
+ // rubric above it - one read instead of one `zen inspect graph` per case.
18
+ //
19
+ // `memory` is what each case did with memory: recalls, searches, commits, and
20
+ // how many committed nodes the graph already held. In a with-memory run every
21
+ // one of those is a call spent on nothing.
22
+ //
23
+ // `commits`, `recalls` and `answers` are the reading behind those numbers:
24
+ // the nodes each case committed (against the candidate, with re-commits
25
+ // marked, when it had memory), every memory step it took, and what it answered.
26
+ // They go through `zen memory` and `zen inspect`, never the files on disk.
27
+ //
28
+ // Run it from anywhere; it finds the project root from its own location.
29
+ // See .github/skills/zen-finetune/SKILL.md.
30
+
31
+ import { spawnSync } from 'node:child_process';
32
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
33
+ import { dirname, join, resolve } from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+
36
+ const NAME = 'report.mjs';
37
+
38
+ const USAGE = `Report on one run's batch: what each case did, and where its trajectory is.
39
+
40
+ ${NAME} index: ok, agent, stop reason, cost per case
41
+ ${NAME} graphs [id...] every trajectory graph, with its rubric above it
42
+ ${NAME} paths [id...] id <TAB> trajectory directory, for zen inspect
43
+ ${NAME} failures only the cases that did not finish, with errors
44
+ ${NAME} oom were commands killed - is this run gradeable at all
45
+ ${NAME} memory per case: recalls, memory searches, commits, already known
46
+ ${NAME} commits [id...] per case: every node it committed; with memory, against the
47
+ candidate, re-commits of what it already held marked
48
+ ${NAME} recalls [id...] per case: every recall and memory tool step, top recalled nodes
49
+ ${NAME} answers [id...] per case: the final answer, with its rubric above it
50
+ ${NAME} compare the per-case table findings.md opens with
51
+
52
+ -d <dir> the batch directory; default the newest finetune/runs/*/batch
53
+ -p <dir> the batch to compare against, for \`compare\`
54
+ -s <file> the dataset, for rubrics; default finetune/dataset.json`;
55
+
56
+ process.stdout.on('error', (err) => {
57
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'EPIPE') {
58
+ process.exit(0);
59
+ }
60
+ });
61
+
62
+ process.chdir(resolve(dirname(fileURLToPath(import.meta.url)), '../../../..'));
63
+
64
+ /** @param {string} msg @param {number} [code] @returns {never} */
65
+ function die(msg, code = 2) {
66
+ console.error(`${NAME}: ${msg}`);
67
+ process.exit(code);
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Arguments
72
+ // ---------------------------------------------------------------------------
73
+
74
+ const MODES = [
75
+ 'index',
76
+ 'graphs',
77
+ 'paths',
78
+ 'failures',
79
+ 'oom',
80
+ 'memory',
81
+ 'commits',
82
+ 'recalls',
83
+ 'answers',
84
+ 'compare',
85
+ ];
86
+
87
+ let mode = '';
88
+ let batch = '';
89
+ let prev = '';
90
+ let dataset = 'finetune/dataset.json';
91
+ /** @type {string[]} */
92
+ const wanted = [];
93
+
94
+ const argv = process.argv.slice(2);
95
+ /** @param {string} flag @returns {string} */
96
+ const next = (flag) => {
97
+ const value = argv.shift();
98
+ if (value === undefined) {
99
+ die(`${flag} needs a value`);
100
+ }
101
+ return value;
102
+ };
103
+
104
+ while (argv.length > 0) {
105
+ const arg = /** @type {string} */ (argv.shift());
106
+ if (arg === '-d') {
107
+ batch = next('-d');
108
+ } else if (arg === '-p') {
109
+ prev = next('-p');
110
+ } else if (arg === '-s') {
111
+ dataset = next('-s');
112
+ } else if (arg === '-h' || arg === '--help') {
113
+ console.log(USAGE);
114
+ process.exit(0);
115
+ } else if (MODES.includes(arg)) {
116
+ if (mode) {
117
+ die('one mode at a time');
118
+ }
119
+ mode = arg;
120
+ } else if (arg.startsWith('-')) {
121
+ console.error(`${NAME}: unknown flag ${arg}`);
122
+ console.error(USAGE);
123
+ process.exit(2);
124
+ } else {
125
+ wanted.push(arg);
126
+ }
127
+ }
128
+
129
+ mode ||= 'index';
130
+
131
+ // Without -d, the batch written most recently - by time, not by name, since
132
+ // `run10` sorts before `run2`.
133
+ if (!batch) {
134
+ let newest = 0;
135
+ /** @type {string[]} */
136
+ let names = [];
137
+ try {
138
+ names = readdirSync('finetune/runs');
139
+ } catch {
140
+ names = [];
141
+ }
142
+ for (const name of names) {
143
+ const dir = join('finetune/runs', name, 'batch');
144
+ const file = join(dir, 'batch.json');
145
+ if (existsSync(file) && statSync(file).mtimeMs > newest) {
146
+ newest = statSync(file).mtimeMs;
147
+ batch = dir;
148
+ }
149
+ }
150
+ }
151
+
152
+ if (!batch || !existsSync(join(batch, 'batch.json'))) {
153
+ console.error(`${NAME}: no batch.json${batch ? ` in ${batch}` : ''}`);
154
+ console.error(' name one with -d, or run a batch first');
155
+ process.exit(2);
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Reading
160
+ // ---------------------------------------------------------------------------
161
+
162
+ /** @param {string} path @returns {any} */
163
+ function json(path) {
164
+ try {
165
+ return JSON.parse(readFileSync(path, 'utf8'));
166
+ } catch {
167
+ return undefined;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * `zen <args> --json`, parsed; `{ error }` when it refused.
173
+ *
174
+ * @param {string[]} args
175
+ * @returns {any}
176
+ */
177
+ function zen(args) {
178
+ const res = spawnSync('zen', [...args, '--json'], {
179
+ encoding: 'utf8',
180
+ maxBuffer: 256 * 1024 * 1024,
181
+ });
182
+ if (res.error) {
183
+ die(`cannot run zen: ${res.error.message}`);
184
+ }
185
+ try {
186
+ const out = JSON.parse(res.stdout);
187
+ return res.status === 0 ? out : { error: out?.error ?? `zen exited ${res.status}` };
188
+ } catch {
189
+ const said = `${res.stderr}\n${res.stdout}`.trim().split('\n').at(-1);
190
+ return { error: said || `zen exited ${res.status}` };
191
+ }
192
+ }
193
+
194
+ /** One line, at most `n` long. @param {string} text @param {number} [n] */
195
+ function flat(text, n = Infinity) {
196
+ const one = String(text).replace(/\s+/g, ' ').trim();
197
+ return one.length > n ? `${one.slice(0, n - 1)}…` : one;
198
+ }
199
+
200
+ const batchDoc = json(join(batch, 'batch.json'));
201
+
202
+ /**
203
+ * `{ id, ok }` per case, in batch order, filtered to the ids asked for.
204
+ *
205
+ * @param {string} dir
206
+ * @returns {{ id: string, ok: boolean }[]}
207
+ */
208
+ function roster(dir) {
209
+ const doc = dir === batch ? batchDoc : json(join(dir, 'batch.json'));
210
+ /** @type {{ id: string, ok: boolean }[]} */
211
+ const all = (doc?.batch_results ?? []).map((/** @type {any} */ r) => ({
212
+ id: String(r.id),
213
+ ok: r.ok === true,
214
+ }));
215
+ return wanted.length === 0 ? all : all.filter((r) => wanted.includes(r.id));
216
+ }
217
+
218
+ /** @param {string} id */
219
+ const output = (id) => json(join(batch, id, 'output.json'));
220
+
221
+ /**
222
+ * @typedef {{ tokens: number, secs: number, llm: number, tools: number, forks: number,
223
+ * recalls: number, reads: number, commits: number, known: number }} Metrics
224
+ */
225
+
226
+ /** @type {Metrics} */
227
+ const ZERO = {
228
+ tokens: 0,
229
+ secs: 0,
230
+ llm: 0,
231
+ tools: 0,
232
+ forks: 0,
233
+ recalls: 0,
234
+ reads: 0,
235
+ commits: 0,
236
+ known: 0,
237
+ };
238
+
239
+ /**
240
+ * What one case cost. The counts come from the graph's own header rows -
241
+ * %% nodes 103 · 33 llm · 30 tool calls · 1 forks
242
+ * %% tools run_command x12, memory_search x2, memory_commit x1
243
+ * - because counting state.json nodes over-counts tool calls about threefold.
244
+ * `recalls` are the automatic recall nodes; `known` is read off the commit
245
+ * results, which say "N already known" when the graph held what was committed.
246
+ *
247
+ * @param {string} dir
248
+ * @param {string} id
249
+ * @returns {Metrics}
250
+ */
251
+ function metrics(dir, id) {
252
+ const out = json(join(dir, id, 'output.json'));
253
+ if (!out) {
254
+ return { ...ZERO };
255
+ }
256
+ const tokens = (out.usage?.inputTokens ?? 0) + (out.usage?.outputTokens ?? 0);
257
+ const secs = Math.floor((out.durationMs ?? 0) / 1000);
258
+ const graph = out.run?.graph;
259
+ if (!graph || !existsSync(graph)) {
260
+ return { ...ZERO, tokens, secs };
261
+ }
262
+ const lines = readFileSync(graph, 'utf8').split('\n');
263
+ const row = lines.find((line) => /^%% nodes\s/.test(line));
264
+ const parts = (row ?? '').replace(/^%% nodes\s+/, '').split(' · ');
265
+ const n = (/** @type {number} */ i) => Number.parseInt(parts[i] ?? '', 10) || 0;
266
+ /** @type {Map<string, number>} */
267
+ const used = new Map();
268
+ for (const m of (lines.find((line) => /^%% tools\s/.test(line)) ?? '').matchAll(
269
+ /([\w.-]+) x(\d+)/g,
270
+ )) {
271
+ used.set(m[1], Number(m[2]));
272
+ }
273
+ const tool = (/** @type {string} */ name) => used.get(name) ?? 0;
274
+ let known = 0;
275
+ for (const line of lines) {
276
+ if (line.includes('memory_commit =')) {
277
+ known += Number(/(\d+) already known/.exec(line)?.[1] ?? 0);
278
+ }
279
+ }
280
+ return {
281
+ tokens,
282
+ secs,
283
+ llm: n(1),
284
+ tools: n(2),
285
+ forks: n(3),
286
+ recalls: lines.filter((line) => /\brecall \d+ nodes\b/.test(line)).length,
287
+ reads: tool('memory_search') + tool('memory_grep') + tool('memory_load'),
288
+ commits: tool('memory_commit'),
289
+ known,
290
+ };
291
+ }
292
+
293
+ /** 1.09M, 812k, 94. @param {number} n */
294
+ function hum(n) {
295
+ if (n >= 1_000_000) {
296
+ return `${(n / 1_000_000).toFixed(2)}M`;
297
+ }
298
+ if (n >= 1000) {
299
+ return `${Math.trunc(n / 1000)}k`;
300
+ }
301
+ return String(n);
302
+ }
303
+
304
+ /**
305
+ * "33 → 30 (−3)" against a previous run, "33" without one. Tokens and seconds
306
+ * move continuously and read as a percentage; calls and forks read as a count.
307
+ *
308
+ * @param {number} was
309
+ * @param {number} now
310
+ * @param {'tok' | 'sec' | 'num'} kind
311
+ * @returns {string}
312
+ */
313
+ function cell(was, now, kind) {
314
+ const show = (/** @type {number} */ v) =>
315
+ kind === 'tok' ? hum(v) : kind === 'sec' ? `${v}s` : String(v);
316
+ if (!prev) {
317
+ return show(now);
318
+ }
319
+ const delta = now - was;
320
+ const size = Math.abs(delta);
321
+ const sign = delta >= 0 ? '+' : '−';
322
+ if (size === 0) {
323
+ return `${show(now)} (=)`;
324
+ }
325
+ if (kind === 'num') {
326
+ return `${show(was)} → ${show(now)} (${sign}${size})`;
327
+ }
328
+ if (was > 0) {
329
+ return `${show(was)} → ${show(now)} (${sign}${((size * 100) / was).toFixed(0)}%)`;
330
+ }
331
+ return `${show(was)} → ${show(now)}`;
332
+ }
333
+
334
+ /**
335
+ * The memory a batch started from, where it is now: `memory.mjs checkpoint`
336
+ * renames `candidate-batchNN` to `after-batchNN` once the batch passes.
337
+ *
338
+ * @param {string} source
339
+ */
340
+ function startedFrom(source) {
341
+ const kept = source.replace(/candidate-(batch\d+)\/?$/, 'after-$1');
342
+ return source && !existsSync(source) && existsSync(kept) ? kept : source;
343
+ }
344
+
345
+ /**
346
+ * `memory off` / `no memory` / `with memory`, for a batch directory.
347
+ *
348
+ * batch.json records `--memory finetune/empty` as mode `copied`, exactly like
349
+ * a with-memory run. What differs is whether the source held a graph.
350
+ *
351
+ * @param {string} dir
352
+ * @returns {{ kind: string, line: string }}
353
+ */
354
+ function memory(dir) {
355
+ const mem = json(join(dir, 'batch.json'))?.batch?.memory ?? {};
356
+ const mode = mem.mode ?? 'none';
357
+ const source = startedFrom(mem.source ?? '');
358
+ if (mode === 'none') {
359
+ return {
360
+ kind: 'memory off',
361
+ line: 'MEMORY OFF — no memory was given to this batch, and none was written',
362
+ };
363
+ }
364
+ if (mode === 'read-only') {
365
+ return {
366
+ kind: 'with memory',
367
+ line: `WITH MEMORY — one shared graph, read-only, every case read it and none wrote (${source})`,
368
+ };
369
+ }
370
+ if (mode === 'copied') {
371
+ return source && existsSync(join(source, 'graph.json'))
372
+ ? {
373
+ kind: 'with memory',
374
+ line: `WITH MEMORY — each case got its own copy of an existing graph (${source})`,
375
+ }
376
+ : {
377
+ kind: 'no memory',
378
+ line: `NO MEMORY — every case started from an empty graph and wrote its own (${source || 'none'})`,
379
+ };
380
+ }
381
+ return { kind: mode, line: `${mode}${source ? ` (${source})` : ''}` };
382
+ }
383
+
384
+ const datasetDoc = json(dataset);
385
+
386
+ /**
387
+ * The rubric from the dataset, joined on the id; empty when there is none.
388
+ *
389
+ * @param {string} id
390
+ * @returns {string[]}
391
+ */
392
+ function rubric(id) {
393
+ const all = datasetDoc?.cases ?? datasetDoc?.samples ?? [];
394
+ const found = all.find((/** @type {any} */ c) => c.id === id);
395
+ return (found?.rubric ?? []).map((/** @type {string} */ line) => ` - ${line}`);
396
+ }
397
+
398
+ /** @param {(string | number)[]} cells @param {number[]} widths */
399
+ const row = (cells, widths) =>
400
+ cells
401
+ .map((c, i) =>
402
+ widths[i] < 0 ? String(c).padStart(-widths[i]) : String(c).padEnd(widths[i]),
403
+ )
404
+ .join(' ')
405
+ .replace(/ +$/, '');
406
+
407
+ const say = (/** @type {string} */ line) => process.stdout.write(`${line}\n`);
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // The modes
411
+ // ---------------------------------------------------------------------------
412
+
413
+ if (mode === 'index') {
414
+ const b = batchDoc?.batch ?? {};
415
+ say(
416
+ `batch ${b.items} items, ${b.ok} ok, ${b.failed} failed` +
417
+ ` · ${Math.floor((b.durationMs ?? 0) / 1000)}s`,
418
+ );
419
+ say(`dir ${batch}`);
420
+ say(`memory ${memory(batch).line}`);
421
+ say('');
422
+ const widths = [24, 5, 12, 12, -7, -5, -6];
423
+ say(row(['ID', 'OK', 'AGENT', 'STOP', 'TOKENS', 'SEC', 'RUBRIC'], widths));
424
+ for (const { id, ok } of roster(batch)) {
425
+ const out = output(id);
426
+ const lines = rubric(id).length;
427
+ say(
428
+ row(
429
+ [
430
+ id,
431
+ String(ok),
432
+ out?.agent || '-',
433
+ out?.stopReason || '-',
434
+ out ? (out.usage?.inputTokens ?? 0) + (out.usage?.outputTokens ?? 0) : '-',
435
+ out ? Math.floor((out.durationMs ?? 0) / 1000) : '-',
436
+ lines === 0 ? '-' : lines,
437
+ ],
438
+ widths,
439
+ ),
440
+ );
441
+ }
442
+ say('');
443
+ say(`next: ${NAME} -d ${batch} graphs | less`);
444
+ } else if (mode === 'graphs') {
445
+ say(`== memory: ${memory(batch).line}`);
446
+ for (const { id, ok } of roster(batch)) {
447
+ const out = output(id);
448
+ say('================================================================');
449
+ say(`== ${id} ok=${ok}`);
450
+ const lines = rubric(id);
451
+ if (lines.length > 0) {
452
+ say('== rubric:');
453
+ for (const line of lines) {
454
+ say(`== ${line}`);
455
+ }
456
+ }
457
+ say('================================================================');
458
+ const graph = out?.run?.graph;
459
+ if (graph && existsSync(graph)) {
460
+ process.stdout.write(readFileSync(graph, 'utf8'));
461
+ } else if (out) {
462
+ say(out.error?.message ?? 'no graph was written');
463
+ } else {
464
+ say('no output.json');
465
+ }
466
+ say('');
467
+ }
468
+ } else if (mode === 'paths') {
469
+ for (const { id } of roster(batch)) {
470
+ say(`${id}\t${output(id)?.run?.dir || '-'}`);
471
+ }
472
+ } else if (mode === 'failures') {
473
+ const items = roster(batch);
474
+ if (items.every((r) => r.ok)) {
475
+ say('nothing failed');
476
+ process.exit(0);
477
+ }
478
+ for (const { id, ok } of items) {
479
+ if (ok) {
480
+ continue;
481
+ }
482
+ say(`-- ${id}`);
483
+ const out = output(id);
484
+ if (out) {
485
+ say(` ${out.error?.message ?? 'unknown'}`);
486
+ if (out.error?.hint) {
487
+ say(` hint: ${out.error.hint}`);
488
+ }
489
+ }
490
+ }
491
+ } else if (mode === 'compare') {
492
+ if (prev && !existsSync(join(prev, 'batch.json'))) {
493
+ die(`no batch.json in ${prev}`);
494
+ }
495
+
496
+ // Part of the table, not a footnote: a pasted table loses what follows it.
497
+ say(`memory: ${memory(batch).line}`);
498
+ if (prev) {
499
+ say(`prev memory: ${memory(prev).line}`);
500
+ }
501
+ say('');
502
+ say(
503
+ '| case | verdict | tokens | llm calls | tool calls | forks | memory reads | commits (known) | time |',
504
+ );
505
+ say('| --- | --- | --- | --- | --- | --- | --- | --- | --- |');
506
+
507
+ const sum = { ...ZERO };
508
+ const was = { ...ZERO };
509
+ /** @param {number} c @param {number} k */
510
+ const commits = (c, k) => (k > 0 ? `${c} (${k} known)` : String(c));
511
+ for (const { id, ok } of roster(batch)) {
512
+ const now = metrics(batch, id);
513
+ const before = prev ? metrics(prev, id) : { ...ZERO };
514
+ for (const k of /** @type {(keyof Metrics)[]} */ (Object.keys(ZERO))) {
515
+ sum[k] += now[k];
516
+ was[k] += before[k];
517
+ }
518
+ // The verdict is the grader's: no script can read it off a trajectory.
519
+ say(
520
+ `| ${id} | ${ok ? '?' : '**did not finish**'} ` +
521
+ `| ${cell(before.tokens, now.tokens, 'tok')} ` +
522
+ `| ${cell(before.llm, now.llm, 'num')} ` +
523
+ `| ${cell(before.tools, now.tools, 'num')} ` +
524
+ `| ${cell(before.forks, now.forks, 'num')} ` +
525
+ `| ${cell(before.recalls + before.reads, now.recalls + now.reads, 'num')} ` +
526
+ `| ${prev ? `${commits(before.commits, before.known)} → ` : ''}${commits(now.commits, now.known)} ` +
527
+ `| ${cell(before.secs, now.secs, 'sec')} |`,
528
+ );
529
+ }
530
+
531
+ say(
532
+ `| **total** | — ` +
533
+ `| **${cell(was.tokens, sum.tokens, 'tok')}** ` +
534
+ `| **${cell(was.llm, sum.llm, 'num')}** ` +
535
+ `| **${cell(was.tools, sum.tools, 'num')}** ` +
536
+ `| **${cell(was.forks, sum.forks, 'num')}** ` +
537
+ `| **${cell(was.recalls + was.reads, sum.recalls + sum.reads, 'num')}** ` +
538
+ `| **${prev ? `${commits(was.commits, was.known)} → ` : ''}${commits(sum.commits, sum.known)}** ` +
539
+ `| **${cell(was.secs, sum.secs, 'sec')}** |`,
540
+ );
541
+
542
+ say('');
543
+ say(`this ${batch}`);
544
+ if (prev) {
545
+ say(`prev ${prev}`);
546
+ }
547
+ say('note: wall time is concurrent — the totals are machine load, not a sum anybody waited.');
548
+ const a = memory(batch).kind;
549
+ const b = prev ? memory(prev).kind : a;
550
+ if (a !== b) {
551
+ say('');
552
+ say(`note: these two runs did not have the same memory (${b} → ${a}).`);
553
+ say(' A with-memory run against the no-memory run that passed the same batch is');
554
+ say(' the measurement of what memory saves, and reads as one; otherwise the wrong');
555
+ say(' -p was given, or the numbers will be read as the effect of an instruction');
556
+ say(' that did nothing.');
557
+ }
558
+ } else if (mode === 'memory') {
559
+ // What each case did with memory. With memory, a read that found nothing
560
+ // and a commit of something the graph already held are both wasted calls.
561
+ say(`memory ${memory(batch).line}`);
562
+ say('');
563
+ const widths = [32, -8, -6, -8, -6];
564
+ say(row(['CASE', 'RECALLS', 'READS', 'COMMITS', 'KNOWN'], widths));
565
+ const sum = { ...ZERO };
566
+ for (const { id } of roster(batch)) {
567
+ const m = metrics(batch, id);
568
+ for (const k of /** @type {(keyof Metrics)[]} */ (Object.keys(ZERO))) {
569
+ sum[k] += m[k];
570
+ }
571
+ say(row([id, m.recalls, m.reads, m.commits, m.known], widths));
572
+ }
573
+ say(row(['total', sum.recalls, sum.reads, sum.commits, sum.known], widths));
574
+ say('');
575
+ say('recalls automatic recall nodes - what memory put in front of the model');
576
+ say('reads memory_search + memory_grep + memory_load calls');
577
+ say('commits memory_commit calls');
578
+ say('known committed nodes the graph already held, as the commit results report them');
579
+ if (memory(batch).kind === 'with memory' && sum.known > 0) {
580
+ say('');
581
+ say(`${sum.known} committed node(s) were already known: a call spent re-saying what`);
582
+ say('the graph held. Find the commit nodes and ask why it did not check first.');
583
+ }
584
+ } else if (mode === 'commits') {
585
+ // Without memory a case starts empty, so everything in its graph it committed.
586
+ // With memory its graph is the candidate plus its commits: diff against it.
587
+ const mem = memory(batch);
588
+ const against =
589
+ mem.kind === 'with memory' ? startedFrom(batchDoc?.batch?.memory?.source ?? '') : '';
590
+ say(`memory ${mem.line}`);
591
+ let added = 0;
592
+ let twins = 0;
593
+ for (const { id } of roster(batch)) {
594
+ const dir = join(batch, id, 'memory');
595
+ say('');
596
+ if (!existsSync(join(dir, 'manifest.json'))) {
597
+ say(`-- ${id} committed nothing`);
598
+ continue;
599
+ }
600
+ const out = against
601
+ ? zen(['memory', 'diff', against, '--dir', dir])
602
+ : zen(['memory', 'ls', '--dir', dir, '--limit', '100000']);
603
+ if (out?.error) {
604
+ say(`-- ${id} ${out.error}`);
605
+ continue;
606
+ }
607
+ /** @type {{ node: any, nearest?: { id: string, score: number }, twin?: boolean }[]} */
608
+ const rows = against ? out.added : out.nodes.map((/** @type {any} */ node) => ({ node }));
609
+ const held = rows.filter((r) => r.twin).length;
610
+ added += rows.length;
611
+ twins += held;
612
+ const extra = against
613
+ ? [
614
+ held ? `${held} already held` : '',
615
+ out.revised.length ? `${out.revised.length} revised` : '',
616
+ out.used.length ? `${out.used.length} loaded` : '',
617
+ ].filter(Boolean)
618
+ : [];
619
+ say(`-- ${id} ${rows.length} committed${extra.length ? `, ${extra.join(', ')}` : ''}`);
620
+ for (const { node, nearest, twin } of rows) {
621
+ const near = nearest
622
+ ? ` ${twin ? 'ALREADY HELD as' : 'nearest'} ${nearest.id} ${nearest.score.toFixed(2)}`
623
+ : '';
624
+ const file = node.file ? ` file .${node.file.format}` : '';
625
+ say(` ${node.id} ${node.kind} ${node.audience.join(',')}${file}${near}`);
626
+ say(` ${flat(node.text)}`);
627
+ }
628
+ for (const { before, after } of against ? out.revised : []) {
629
+ say(` ${after.id} ${after.kind} revised r${before.revision} -> r${after.revision}`);
630
+ say(` ${flat(after.text)}`);
631
+ }
632
+ for (const { node, loads } of against ? out.used : []) {
633
+ say(` ${node.id} ${node.kind} loaded ${loads}x`);
634
+ say(` ${flat(node.text)}`);
635
+ }
636
+ }
637
+ say('');
638
+ say(`total ${added} committed${against ? `, ${twins} already held` : ''}`);
639
+ if (against) {
640
+ say(`against ${against}`);
641
+ if (twins > 0) {
642
+ say('');
643
+ say(`${twins} commit(s) said again what the candidate held: merge folds them,`);
644
+ say('so each was a call that bought nothing. Ask at the commit why it did not');
645
+ say('recall first.');
646
+ }
647
+ }
648
+ say('');
649
+ say(`one node in full: zen memory show <node-id> --dir ${join(batch, '<case>', 'memory')}`);
650
+ } else if (mode === 'recalls') {
651
+ say(`memory ${memory(batch).line}`);
652
+ const STEP =
653
+ /^\s*(n\d+)\S*"n\d+ ((?:recall \d+ nodes|memory_(?:search|grep|load|commit)) .*?)(?: · t\+|")/;
654
+ for (const { id } of roster(batch)) {
655
+ const out = output(id);
656
+ const graph = out?.run?.graph;
657
+ say('');
658
+ if (!graph || !existsSync(graph)) {
659
+ say(`-- ${id} no graph`);
660
+ continue;
661
+ }
662
+ /** @type {{ node: string, label: string }[]} */
663
+ const steps = [];
664
+ for (const line of readFileSync(graph, 'utf8').split('\n')) {
665
+ const m = STEP.exec(line);
666
+ if (m) {
667
+ steps.push({ node: m[1], label: m[2] });
668
+ }
669
+ }
670
+ if (steps.length === 0) {
671
+ say(`-- ${id} never touched memory`);
672
+ continue;
673
+ }
674
+ const recalls = steps.filter((s) => s.label.startsWith('recall '));
675
+ /** @type {Map<string, string[]>} */
676
+ const seeds = new Map();
677
+ if (recalls.length > 0) {
678
+ const res = zen([
679
+ 'inspect',
680
+ 'node',
681
+ ...recalls.map((r) => r.node),
682
+ '--dir',
683
+ out.run.dir,
684
+ ]);
685
+ for (const n of res?.nodes ?? []) {
686
+ const text = n.parts?.find((/** @type {any} */ p) => p.name === 'recalled')?.text;
687
+ const top = [
688
+ ...String(text ?? '').matchAll(/^(\d\.\d\d) +(\S+) +(\S+)\n +(.+)$/gm),
689
+ ];
690
+ seeds.set(
691
+ n.id,
692
+ top.slice(0, 3).map((m) => `${m[1]} ${m[2]} ${m[3]} ${flat(m[4], 90)}`),
693
+ );
694
+ }
695
+ }
696
+ const calls = steps.filter(
697
+ (s) => !s.label.startsWith('recall ') && !s.label.includes(' = '),
698
+ );
699
+ say(`-- ${id} ${recalls.length} recalls, ${calls.length} memory tool calls`);
700
+ for (const s of steps) {
701
+ say(` ${s.node.padEnd(5)} ${flat(s.label, 120)}`);
702
+ for (const line of seeds.get(s.node) ?? []) {
703
+ say(` ${line}`);
704
+ }
705
+ }
706
+ }
707
+ say('');
708
+ say(
709
+ `one step in full: zen inspect node <nN> --dir "$(${NAME} -d ${batch} paths <case> | cut -f2)"`,
710
+ );
711
+ } else if (mode === 'answers') {
712
+ for (const { id, ok } of roster(batch)) {
713
+ const out = output(id);
714
+ say('================================================================');
715
+ say(`== ${id} ok=${ok}`);
716
+ const lines = rubric(id);
717
+ if (lines.length > 0) {
718
+ say('== rubric:');
719
+ for (const line of lines) {
720
+ say(`== ${line}`);
721
+ }
722
+ }
723
+ say('================================================================');
724
+ if (!out) {
725
+ say('no output.json');
726
+ } else if (out.ok === false) {
727
+ say(`did not finish: ${out.error?.message ?? 'unknown'}`);
728
+ } else {
729
+ say(String(out.output ?? '').trim() || '(no answer)');
730
+ }
731
+ say('');
732
+ }
733
+ } else if (mode === 'oom') {
734
+ // The graph is where an exit code survives: a killed `run_command` reads
735
+ // `= exit code 137`. Never grep the batch directory - a workspace is full of
736
+ // data files with 137 in them.
737
+ /** @type {{ id: string, killed: number, timedout: number }[]} */
738
+ const scan = [];
739
+ for (const { id } of roster(batch)) {
740
+ const graph = output(id)?.run?.graph;
741
+ if (!graph || !existsSync(graph)) {
742
+ continue;
743
+ }
744
+ const lines = readFileSync(graph, 'utf8').split('\n');
745
+ const killed = lines.filter((l) => l.includes('exit code 137')).length;
746
+ const timedout = lines.filter((l) => l.includes('exit code 124')).length;
747
+ if (killed > 0 || timedout > 0) {
748
+ scan.push({ id, killed, timedout });
749
+ }
750
+ }
751
+
752
+ if (scan.length === 0) {
753
+ say('no killed commands in this run — it is gradeable');
754
+ process.exit(0);
755
+ }
756
+
757
+ const widths = [32, -8, -8];
758
+ say(row(['CASE', 'OOM 137', 'TIME 124'], widths));
759
+ for (const { id, killed, timedout } of scan) {
760
+ say(row([id, killed, timedout], widths));
761
+ }
762
+ say('');
763
+
764
+ const casesKilled = scan.filter((s) => s.killed > 0).length;
765
+ const totalKilled = scan.reduce((n, s) => n + s.killed, 0);
766
+ const totalTimedout = scan.reduce((n, s) => n + s.timedout, 0);
767
+ const config = json('finetune/config.json') ?? {};
768
+ const current = Number(config.concurrency) || 16;
769
+ const halved = Math.max(4, Math.floor(current / 2));
770
+
771
+ if (casesKilled > 0) {
772
+ say(`${totalKilled} command(s) across ${casesKilled} case(s) were OOM-killed.`);
773
+ say('');
774
+ say('THIS RUN IS VOID, NOT GRADED.');
775
+ say(' Nothing here is evidence about the prompt. Do not grade it, do not');
776
+ say(' change instructions on it, and do not compare its tokens to another');
777
+ say(' run.');
778
+ say('');
779
+ if (current <= 4) {
780
+ say(` It ran at concurrency ${current}, the floor. The machine cannot run this`);
781
+ say(' batch: stop and tell the user.');
782
+ } else {
783
+ say(` Halve the concurrency and run the same cases again: set`);
784
+ say(
785
+ ` "concurrency": ${halved} in finetune/config.json (was ${current}; never below 4).`,
786
+ );
787
+ }
788
+ process.exit(1);
789
+ }
790
+
791
+ say(`${totalTimedout} command(s) hit the timeout; none was OOM-killed.`);
792
+ say(' Under memory pressure 124 is 137 wearing a different number, and raising');
793
+ say(' the timeout converts one into the other rather than fixing either. If it');
794
+ say(
795
+ ` repeats in the next run, halve the concurrency: ${current} -> ${halved} (never below 4).`,
796
+ );
797
+ }