@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.
Files changed (184) hide show
  1. package/README.md +88 -2
  2. package/dist/ask.d.ts +51 -0
  3. package/dist/ask.js +166 -0
  4. package/dist/banner.js +2 -2
  5. package/dist/batch.d.ts +83 -0
  6. package/dist/batch.js +413 -0
  7. package/dist/catalog.d.ts +11 -1
  8. package/dist/catalog.js +80 -7
  9. package/dist/command.d.ts +6 -0
  10. package/dist/commands/inspect.d.ts +57 -0
  11. package/dist/commands/inspect.js +539 -91
  12. package/dist/commands/list.js +128 -19
  13. package/dist/commands/memory.js +461 -32
  14. package/dist/commands/meta.js +311 -21
  15. package/dist/commands/models.js +110 -14
  16. package/dist/commands/open.js +4 -4
  17. package/dist/commands/run.js +175 -36
  18. package/dist/commands/sandbox.js +148 -15
  19. package/dist/engine.d.ts +24 -0
  20. package/dist/engine.js +61 -8
  21. package/dist/eventlog.d.ts +59 -0
  22. package/dist/eventlog.js +146 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +20 -3
  25. package/dist/main.js +5 -2
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +60 -0
  28. package/dist/meta/finetune/children.d.ts +8 -0
  29. package/dist/meta/finetune/children.js +76 -0
  30. package/dist/meta/finetune/command.d.ts +30 -0
  31. package/dist/meta/finetune/command.js +338 -0
  32. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  33. package/dist/meta/finetune/dataset/anchors.js +239 -0
  34. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  35. package/dist/meta/finetune/dataset/changes.js +319 -0
  36. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  37. package/dist/meta/finetune/dataset/command.js +1011 -0
  38. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  39. package/dist/meta/finetune/dataset/drift.js +71 -0
  40. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  41. package/dist/meta/finetune/dataset/export.js +51 -0
  42. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  43. package/dist/meta/finetune/dataset/index.js +2 -0
  44. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  45. package/dist/meta/finetune/dataset/sample.js +197 -0
  46. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/schema.js +216 -0
  48. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  49. package/dist/meta/finetune/dataset/store.js +177 -0
  50. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  51. package/dist/meta/finetune/dataset/types.js +39 -0
  52. package/dist/meta/finetune/failure.d.ts +15 -0
  53. package/dist/meta/finetune/failure.js +26 -0
  54. package/dist/meta/finetune/feedback.d.ts +45 -0
  55. package/dist/meta/finetune/feedback.js +85 -0
  56. package/dist/meta/finetune/improvements.d.ts +51 -0
  57. package/dist/meta/finetune/improvements.js +244 -0
  58. package/dist/meta/finetune/index.d.ts +2 -0
  59. package/dist/meta/finetune/index.js +2 -0
  60. package/dist/meta/finetune/journal.d.ts +35 -0
  61. package/dist/meta/finetune/journal.js +135 -0
  62. package/dist/meta/finetune/live.d.ts +74 -0
  63. package/dist/meta/finetune/live.js +226 -0
  64. package/dist/meta/finetune/loop.d.ts +3 -0
  65. package/dist/meta/finetune/loop.js +114 -0
  66. package/dist/meta/finetune/memories.d.ts +47 -0
  67. package/dist/meta/finetune/memories.js +154 -0
  68. package/dist/meta/finetune/report.d.ts +33 -0
  69. package/dist/meta/finetune/report.js +1284 -0
  70. package/dist/meta/finetune/retry.d.ts +24 -0
  71. package/dist/meta/finetune/retry.js +96 -0
  72. package/dist/meta/finetune/sampler.d.ts +18 -0
  73. package/dist/meta/finetune/sampler.js +41 -0
  74. package/dist/meta/finetune/system.d.ts +27 -0
  75. package/dist/meta/finetune/system.js +132 -0
  76. package/dist/meta/finetune/train.d.ts +39 -0
  77. package/dist/meta/finetune/train.js +391 -0
  78. package/dist/meta/finetune/tui.d.ts +16 -0
  79. package/dist/meta/finetune/tui.js +291 -0
  80. package/dist/meta/finetune/tuning.d.ts +167 -0
  81. package/dist/meta/finetune/tuning.js +289 -0
  82. package/dist/meta/finetune/usage.d.ts +62 -0
  83. package/dist/meta/finetune/usage.js +225 -0
  84. package/dist/meta/host.d.ts +14 -0
  85. package/dist/meta/host.js +20 -0
  86. package/dist/meta/index.d.ts +11 -0
  87. package/dist/meta/index.js +11 -0
  88. package/dist/meta/launch.d.ts +81 -0
  89. package/dist/meta/launch.js +286 -0
  90. package/dist/meta/model.d.ts +44 -0
  91. package/dist/meta/model.js +119 -0
  92. package/dist/meta/prompts.d.ts +18 -0
  93. package/dist/meta/prompts.js +61 -0
  94. package/dist/meta/relay.d.ts +20 -0
  95. package/dist/meta/relay.js +123 -0
  96. package/dist/meta/tokens.d.ts +52 -0
  97. package/dist/meta/tokens.js +226 -0
  98. package/dist/meta/window.d.ts +61 -0
  99. package/dist/meta/window.js +198 -0
  100. package/dist/meta/wire.d.ts +42 -0
  101. package/dist/meta/wire.js +129 -0
  102. package/dist/modelref.d.ts +16 -0
  103. package/dist/modelref.js +41 -0
  104. package/dist/narrate.js +5 -2
  105. package/dist/podman.d.ts +42 -0
  106. package/dist/podman.js +198 -5
  107. package/dist/progress.d.ts +33 -0
  108. package/dist/progress.js +402 -0
  109. package/dist/projects.d.ts +0 -5
  110. package/dist/projects.js +2 -15
  111. package/dist/request.d.ts +45 -0
  112. package/dist/request.js +300 -0
  113. package/dist/sandbox.d.ts +2 -0
  114. package/dist/sandbox.js +20 -3
  115. package/dist/scaffold.d.ts +15 -6
  116. package/dist/scaffold.js +5 -60
  117. package/dist/session.d.ts +26 -1
  118. package/dist/session.js +46 -32
  119. package/dist/term.d.ts +5 -0
  120. package/dist/term.js +28 -3
  121. package/dist/trace.d.ts +84 -0
  122. package/dist/trace.js +737 -0
  123. package/dist/tui/models.d.ts +33 -0
  124. package/dist/tui/models.js +389 -0
  125. package/dist/tui/wrap.d.ts +5 -0
  126. package/dist/tui/wrap.js +12 -0
  127. package/dist/usage.d.ts +78 -0
  128. package/dist/usage.js +51 -0
  129. package/dist/validate.js +25 -3
  130. package/package.json +3 -2
  131. package/templates/editor/.github/copilot-instructions.md +41 -107
  132. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  133. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  134. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  135. package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
  136. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  137. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  138. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  139. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  140. package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
  141. package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
  142. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  143. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  144. package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
  145. package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
  146. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  147. package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
  148. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  149. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  150. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  151. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  152. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  153. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  154. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  155. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1791 -0
  156. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  157. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  158. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  159. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  160. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  161. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  162. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  163. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  164. package/templates/editor/.github/skills/zen-inspect/SKILL.md +431 -0
  165. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  166. package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
  167. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
  168. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  169. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  170. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  171. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
  172. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  173. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  174. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  175. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  176. package/templates/editor/.vscode/agents.schema.json +1 -1
  177. package/templates/editor/.vscode/settings.json +2 -0
  178. package/templates/project/agents/files-instructions.md +35 -13
  179. package/templates/project/agents/fork-instructions.md +27 -0
  180. package/templates/project/agents/memory-instructions.md +106 -34
  181. package/templates/project/agents/tools-instructions.md +7 -4
  182. package/templates/project/gitignore +5 -0
  183. package/dist/meta.d.ts +0 -221
  184. package/dist/meta.js +0 -684
@@ -0,0 +1,722 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ //
4
+ // What the tuning has cost, in tokens: finetune/USAGE.md and finetune/usage.json.
5
+ //
6
+ // Three things spend tokens during a tuning, and only one of them is the
7
+ // project under test:
8
+ //
9
+ // project agents every case of every run - read off each run's batch.json
10
+ // and output.json, so runs from before the ledger count too
11
+ // meta agent the agent doing the tuning - one ledger row per model call,
12
+ // from the spans `zen meta` records as it runs
13
+ // inspect ask every `zen inspect ask` the grading made - one row each
14
+ //
15
+ // The ledger is finetune/usage/ledger.jsonl. `zen meta`, `zen run`, `zen run
16
+ // batch` and `zen inspect ask` append to it whenever the project has a
17
+ // finetune/; nothing here writes to it.
18
+ //
19
+ // A meta agent call is put on the stage it happened in, by time: while a run
20
+ // executes it is "waiting" on that run, after it until the next run starts it is
21
+ // "grading" it, and before the first run it is "setup". An ask goes to the run
22
+ // whose trajectory it asked about.
23
+ //
24
+ // The report is rewritten whole every time - by next.mjs, and by a running
25
+ // `zen meta` every few minutes. Never edit it by hand.
26
+ //
27
+ // Run it from anywhere; it finds the project root from its own location.
28
+ // See .github/skills/zen-finetune/SKILL.md, "The usage report".
29
+
30
+ import {
31
+ existsSync,
32
+ mkdirSync,
33
+ readdirSync,
34
+ readFileSync,
35
+ renameSync,
36
+ writeFileSync,
37
+ } from 'node:fs';
38
+ import { dirname, join, resolve } from 'node:path';
39
+ import { fileURLToPath } from 'node:url';
40
+
41
+ const NAME = 'usage.mjs';
42
+
43
+ const USAGE = `What the tuning has cost, in tokens: finetune/USAGE.md and finetune/usage.json.
44
+
45
+ ${NAME} write both, and print one summary line
46
+ ${NAME} -q write both, print nothing
47
+ ${NAME} --stdout print the markdown instead of writing anything`;
48
+
49
+ process.stdout.on('error', (err) => {
50
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'EPIPE') {
51
+ process.exit(0);
52
+ }
53
+ });
54
+
55
+ process.chdir(resolve(dirname(fileURLToPath(import.meta.url)), '../../../..'));
56
+
57
+ const RUNS = 'finetune/runs';
58
+ const LEDGER = 'finetune/usage/ledger.jsonl';
59
+ const REPORT = 'finetune/USAGE.md';
60
+ const DATA = 'finetune/usage.json';
61
+ const RUN_NAME = /^batch(\d{2})-(nomem|mem)-run(\d+)$/;
62
+ const FINAL = 'final-check';
63
+ const COSTLIEST = 10;
64
+
65
+ let quiet = false;
66
+ let toStdout = false;
67
+ for (const arg of process.argv.slice(2)) {
68
+ if (arg === '-q') {
69
+ quiet = true;
70
+ } else if (arg === '--stdout') {
71
+ toStdout = true;
72
+ } else if (arg === '-h' || arg === '--help') {
73
+ console.log(USAGE);
74
+ process.exit(0);
75
+ } else {
76
+ console.error(`${NAME}: unknown argument ${arg}`);
77
+ console.error(USAGE);
78
+ process.exit(2);
79
+ }
80
+ }
81
+
82
+ if (!existsSync('finetune')) {
83
+ console.error(`${NAME}: no finetune/ - nothing is being tuned here`);
84
+ process.exit(2);
85
+ }
86
+
87
+ /** @param {string} path @returns {any} */
88
+ function json(path) {
89
+ try {
90
+ return JSON.parse(readFileSync(path, 'utf8'));
91
+ } catch {
92
+ return undefined;
93
+ }
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Tokens
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Calls and tokens. `cached` is part of `input`, `reasoning` part of `output`.
102
+ *
103
+ * @typedef {{ calls: number, input: number, cached: number, output: number, reasoning: number }} Spend
104
+ */
105
+
106
+ /** @returns {Spend} */
107
+ const zero = () => ({ calls: 0, input: 0, cached: 0, output: 0, reasoning: 0 });
108
+
109
+ /**
110
+ * A zen `TokenUsage` and a call count, as a Spend.
111
+ *
112
+ * @param {any} usage
113
+ * @param {number} calls
114
+ * @returns {Spend}
115
+ */
116
+ const spendOf = (usage, calls) => ({
117
+ calls,
118
+ input: Number(usage?.inputTokens ?? 0),
119
+ cached: Number(usage?.cachedInputTokens ?? 0),
120
+ output: Number(usage?.outputTokens ?? 0),
121
+ reasoning: Number(usage?.reasoningTokens ?? 0),
122
+ });
123
+
124
+ /** @param {Spend} into @param {Spend} add */
125
+ function plus(into, add) {
126
+ into.calls += add.calls;
127
+ into.input += add.input;
128
+ into.cached += add.cached;
129
+ into.output += add.output;
130
+ into.reasoning += add.reasoning;
131
+ return into;
132
+ }
133
+
134
+ /** @param {Map<string, Spend>} map @param {string} key @param {Spend} add */
135
+ function bump(map, key, add) {
136
+ map.set(key, plus(map.get(key) ?? zero(), add));
137
+ }
138
+
139
+ /** @param {Spend} s */
140
+ const total = (s) => s.input + s.output;
141
+
142
+ /** 950, 12.3k, 1.84M. @param {number} n */
143
+ function hum(n) {
144
+ if (n >= 1e9) {
145
+ return `${(n / 1e9).toFixed(2)}B`;
146
+ }
147
+ if (n >= 1e6) {
148
+ return `${(n / 1e6).toFixed(2)}M`;
149
+ }
150
+ if (n >= 1e3) {
151
+ return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
152
+ }
153
+ return String(Math.round(n));
154
+ }
155
+
156
+ /** @param {number} part @param {number} whole */
157
+ const pct = (part, whole) => (whole > 0 ? `${Math.round((part * 100) / whole)}%` : '-');
158
+
159
+ /** @param {number} ms */
160
+ function clock(ms) {
161
+ const s = Math.round(ms / 1000);
162
+ const h = Math.floor(s / 3600);
163
+ const m = Math.floor((s % 3600) / 60);
164
+ return h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${s % 60}s` : `${s}s`;
165
+ }
166
+
167
+ /**
168
+ * The cells every token table shares.
169
+ *
170
+ * @param {Spend} s
171
+ * @returns {string[]}
172
+ */
173
+ const cells = (s) => [
174
+ String(s.calls),
175
+ hum(s.input),
176
+ pct(s.cached, s.input),
177
+ hum(s.output),
178
+ hum(s.reasoning),
179
+ hum(total(s)),
180
+ ];
181
+ const HEAD = ['calls', 'input', 'cached', 'output', 'reasoning', 'total'];
182
+
183
+ /** @param {string[]} head @param {string[][]} rows */
184
+ function table(head, rows) {
185
+ if (rows.length === 0) {
186
+ return ['_none yet_'];
187
+ }
188
+ const right = head.map((_, i) =>
189
+ rows.every((r) => /^[−\d.,%kMB+-]+[smh]?$|^-$/.test(r[i] ?? '')),
190
+ );
191
+ return [
192
+ `| ${head.join(' | ')} |`,
193
+ `| ${head.map((_, i) => (right[i] ? '---:' : '---')).join(' | ')} |`,
194
+ ...rows.map((r) => `| ${r.join(' | ')} |`),
195
+ ];
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Reading
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * @typedef {{ name: string, batch: number, kind: 'nomem' | 'mem' | 'final', run: number,
204
+ * start: number, end: number, durationMs: number, items: number, ok: number,
205
+ * verdict: string, models: Map<string, Spend>, spend: Spend }} Run
206
+ */
207
+
208
+ /** @param {string} dir */
209
+ function verdict(dir) {
210
+ const path = join(dir, 'findings.md');
211
+ const m = existsSync(path) ? /^## ([A-Z][A-Z ]*[A-Z])/m.exec(readFileSync(path, 'utf8')) : null;
212
+ return m ? m[1].trimEnd() : '';
213
+ }
214
+
215
+ /** Every run that finished, in the order it started. @type {Run[]} */
216
+ const runs = [];
217
+ /** run dir of a case's trajectory -> [finetune run, case id] @type {Map<string, [string, string]>} */
218
+ const trajectories = new Map();
219
+ /** @type {{ run: string, id: string, spend: Spend, durationMs: number }[]} */
220
+ const cases = [];
221
+
222
+ for (const name of existsSync(RUNS) ? readdirSync(RUNS) : []) {
223
+ const m = RUN_NAME.exec(name);
224
+ if (!m && name !== FINAL) {
225
+ continue;
226
+ }
227
+ const doc = json(join(RUNS, name, 'batch', 'batch.json'));
228
+ if (!doc?.batch) {
229
+ continue;
230
+ }
231
+ const b = doc.batch;
232
+ /** @type {Map<string, Spend>} */
233
+ const models = new Map();
234
+ for (const one of b.models ?? []) {
235
+ bump(models, one.model, spendOf(one.usage, Number(one.calls ?? 0)));
236
+ }
237
+ const spend = [...models.values()].reduce(plus, zero());
238
+ runs.push({
239
+ name,
240
+ batch: m ? +m[1] : Infinity,
241
+ kind: m ? /** @type {'nomem' | 'mem'} */ (m[2]) : 'final',
242
+ run: m ? +m[3] : 1,
243
+ start: Date.parse(b.startedAt),
244
+ end: Date.parse(b.finishedAt),
245
+ durationMs: Number(b.durationMs ?? 0),
246
+ items: Number(b.items ?? 0),
247
+ ok: Number(b.ok ?? 0),
248
+ verdict: verdict(join(RUNS, name)),
249
+ models,
250
+ spend,
251
+ });
252
+ for (const r of doc.batch_results ?? []) {
253
+ const out = json(join(RUNS, name, 'batch', String(r.id), 'output.json'));
254
+ if (!out) {
255
+ continue;
256
+ }
257
+ if (out.run?.dir) {
258
+ trajectories.set(resolve(out.run.dir), [name, String(r.id)]);
259
+ }
260
+ if (out.usage) {
261
+ cases.push({
262
+ run: name,
263
+ id: String(r.id),
264
+ spend: spendOf(out.usage, 0),
265
+ durationMs: Number(out.durationMs ?? 0),
266
+ });
267
+ }
268
+ }
269
+ }
270
+ runs.sort((a, b) => a.start - b.start);
271
+ const byName = new Map(runs.map((r) => [r.name, r]));
272
+
273
+ /** @type {any[]} */
274
+ const ledger = existsSync(LEDGER)
275
+ ? readFileSync(LEDGER, 'utf8')
276
+ .split('\n')
277
+ .filter((l) => l.trim())
278
+ .flatMap((l) => {
279
+ try {
280
+ return [JSON.parse(l)];
281
+ } catch {
282
+ return [];
283
+ }
284
+ })
285
+ : [];
286
+ const since = ledger.length > 0 ? ledger.map((r) => String(r.ts)).sort()[0] : undefined;
287
+
288
+ /** The finetune run a batch directory belongs to. @param {string | undefined} dir */
289
+ function runOfBatchDir(dir) {
290
+ const m = /\finetune\/runs\/([^/]+)\/batch\/?$/.exec(dir ?? '');
291
+ return m && byName.has(m[1]) ? m[1] : undefined;
292
+ }
293
+
294
+ /**
295
+ * Which stage a moment of the tuning belongs to: `waiting <run>` while it ran,
296
+ * `grading <run>` from its end until the next run starts, `setup` before any.
297
+ *
298
+ * @param {number} t
299
+ * @returns {{ stage: string, run?: string }}
300
+ */
301
+ function stageAt(t) {
302
+ let last;
303
+ for (const r of runs) {
304
+ if (t >= r.start && t <= r.end) {
305
+ return { stage: 'waiting', run: r.name };
306
+ }
307
+ if (r.end < t) {
308
+ last = r;
309
+ }
310
+ }
311
+ return last ? { stage: 'grading', run: last.name } : { stage: 'setup' };
312
+ }
313
+
314
+ /** The batch label a run belongs to: `batch 3`, `final check`, `setup`. @param {string | undefined} run */
315
+ function batchOf(run) {
316
+ const r = run ? byName.get(run) : undefined;
317
+ if (!r) {
318
+ return 'setup';
319
+ }
320
+ return r.kind === 'final' ? 'final check' : `batch ${r.batch}`;
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Folding
325
+ // ---------------------------------------------------------------------------
326
+
327
+ /** actor -> model -> spend @type {Map<string, Map<string, Spend>>} */
328
+ const actors = new Map();
329
+ /** @param {string} actor @param {string} model @param {Spend} s */
330
+ function spend(actor, model, s) {
331
+ const models = actors.get(actor) ?? new Map();
332
+ bump(models, model, s);
333
+ actors.set(actor, models);
334
+ }
335
+ const PROJECT = 'project agents';
336
+ const META = 'meta agent';
337
+ const ASK = 'inspect ask';
338
+ const SINGLE = 'zen run (single)';
339
+
340
+ for (const r of runs) {
341
+ for (const [model, s] of r.models) {
342
+ spend(PROJECT, model, s);
343
+ }
344
+ }
345
+
346
+ /** finetune run -> what the meta agent and the asks spent on it @type {Map<string, { meta: Spend, waiting: Spend, ask: Spend, asks: number }>} */
347
+ const onRun = new Map();
348
+ const runSide = (/** @type {string} */ name) => {
349
+ const had = onRun.get(name) ?? { meta: zero(), waiting: zero(), ask: zero(), asks: 0 };
350
+ onRun.set(name, had);
351
+ return had;
352
+ };
353
+ /** stage label -> meta spend @type {Map<string, Spend>} */
354
+ const metaStages = new Map();
355
+ /** case -> llm calls, where the ledger has them @type {Map<string, number>} */
356
+ const caseCalls = new Map();
357
+
358
+ /**
359
+ * @typedef {{ session: string, first: string, last: string, resumes: number, exitCode: number,
360
+ * spans: Spend, totals?: Spend, models?: any[] }} MetaSession
361
+ */
362
+ /** @type {Map<string, MetaSession>} */
363
+ const sessions = new Map();
364
+ /** @param {string} id @param {string} ts */
365
+ const sessionOf = (id, ts) => {
366
+ const had = sessions.get(id) ?? {
367
+ session: id,
368
+ first: ts,
369
+ last: ts,
370
+ resumes: 0,
371
+ exitCode: 0,
372
+ spans: zero(),
373
+ };
374
+ had.first = ts < had.first ? ts : had.first;
375
+ had.last = ts > had.last ? ts : had.last;
376
+ sessions.set(id, had);
377
+ return had;
378
+ };
379
+
380
+ for (const row of ledger) {
381
+ if (row.kind === 'meta.call') {
382
+ const s = spendOf(row.usage, 1);
383
+ spend(META, String(row.model), s);
384
+ const { stage, run } = stageAt(Date.parse(row.startedAt));
385
+ bump(metaStages, run ? `${batchOf(run)} · ${stage} ${run}` : stage, s);
386
+ if (run) {
387
+ plus(stage === 'waiting' ? runSide(run).waiting : runSide(run).meta, s);
388
+ }
389
+ if (row.session) {
390
+ plus(sessionOf(String(row.session), String(row.ts)).spans, s);
391
+ }
392
+ } else if (row.kind === 'meta.session') {
393
+ const one = sessionOf(String(row.session), String(row.ts));
394
+ // Cumulative over resumes: the latest row is the whole session.
395
+ if (!one.models || String(row.ts) >= one.last) {
396
+ one.models = row.models;
397
+ one.totals = (row.models ?? []).reduce(
398
+ (/** @type {Spend} */ acc, /** @type {any} */ m) =>
399
+ plus(acc, spendOf(m.usage, Number(m.calls ?? 0))),
400
+ zero(),
401
+ );
402
+ one.exitCode = Number(row.exitCode ?? 0);
403
+ }
404
+ one.resumes = Math.max(one.resumes, Number(row.resumes ?? 0));
405
+ } else if (row.kind === 'ask') {
406
+ const s = spendOf(row.usage, 1);
407
+ spend(ASK, String(row.model), s);
408
+ const run =
409
+ trajectories.get(resolve(String(row.runDir)))?.[0] ??
410
+ stageAt(Date.parse(row.startedAt)).run;
411
+ if (run) {
412
+ plus(runSide(run).ask, s);
413
+ runSide(run).asks += 1;
414
+ }
415
+ } else if (row.kind === 'run') {
416
+ const run = runOfBatchDir(row.batchDir);
417
+ if (!row.batchDir) {
418
+ for (const m of row.models ?? []) {
419
+ spend(SINGLE, String(m.model), spendOf(m.usage, Number(m.calls ?? 0)));
420
+ }
421
+ }
422
+ if (run) {
423
+ caseCalls.set(
424
+ `${run}/${row.item}`,
425
+ (row.models ?? []).reduce(
426
+ (/** @type {number} */ n, /** @type {any} */ m) => n + Number(m.calls ?? 0),
427
+ 0,
428
+ ),
429
+ );
430
+ }
431
+ }
432
+ }
433
+
434
+ /** @param {Map<string, Spend>} models */
435
+ const sum = (models) => [...models.values()].reduce((acc, s) => plus(acc, { ...s }), zero());
436
+ /** @param {Map<string, Spend>} map */
437
+ const heaviest = (map) => [...map.entries()].sort((a, b) => total(b[1]) - total(a[1]));
438
+
439
+ const ORDER = [PROJECT, META, ASK, SINGLE];
440
+ const totals = new Map(
441
+ ORDER.filter((a) => actors.has(a)).map((a) => [
442
+ a,
443
+ sum(/** @type {Map<string, Spend>} */ (actors.get(a))),
444
+ ]),
445
+ );
446
+ const grand = [...totals.values()].reduce((acc, s) => plus(acc, { ...s }), zero());
447
+
448
+ /** @type {Map<string, Spend>} */
449
+ const overall = new Map();
450
+ for (const models of actors.values()) {
451
+ for (const [model, s] of models) {
452
+ bump(overall, model, { ...s });
453
+ }
454
+ }
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // Batches
458
+ // ---------------------------------------------------------------------------
459
+
460
+ /**
461
+ * @typedef {{ label: string, nomem: Run[], mem: Run[], project: Spend, meta: Spend, ask: Spend,
462
+ * asks: number, saving: string }} Batch
463
+ */
464
+ /** @type {Map<string, Batch>} */
465
+ const batches = new Map();
466
+ for (const r of runs) {
467
+ const label = batchOf(r.name);
468
+ const b = batches.get(label) ?? {
469
+ label,
470
+ nomem: [],
471
+ mem: [],
472
+ project: zero(),
473
+ meta: zero(),
474
+ ask: zero(),
475
+ asks: 0,
476
+ saving: '-',
477
+ };
478
+ (r.kind === 'mem' ? b.mem : b.nomem).push(r);
479
+ plus(b.project, r.spend);
480
+ const side = onRun.get(r.name);
481
+ if (side) {
482
+ plus(b.meta, side.meta);
483
+ plus(b.meta, side.waiting);
484
+ plus(b.ask, side.ask);
485
+ b.asks += side.asks;
486
+ }
487
+ batches.set(label, b);
488
+ }
489
+ for (const b of batches.values()) {
490
+ const lastNomem = b.nomem.at(-1);
491
+ const lastMem = b.mem.at(-1);
492
+ if (lastNomem && lastMem && total(lastNomem.spend) > 0) {
493
+ const delta = 1 - total(lastMem.spend) / total(lastNomem.spend);
494
+ b.saving = `${delta >= 0 ? '−' : '+'}${Math.abs(Math.round(delta * 100))}%`;
495
+ }
496
+ }
497
+
498
+ const setup = metaStages.get('setup') ?? zero();
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // The report
502
+ // ---------------------------------------------------------------------------
503
+
504
+ const now = new Date().toISOString();
505
+ /** @type {string[]} */
506
+ const md = [
507
+ '<!-- Written by .github/skills/zen-finetune/scripts/usage.mjs - do not edit; it is rewritten whole. -->',
508
+ '',
509
+ '# Token usage',
510
+ '',
511
+ `Updated ${now.replace('T', ' ').slice(0, 16)} UTC. ` +
512
+ (since
513
+ ? `Meta agent and ask accounting since ${since.replace('T', ' ').slice(0, 16)} UTC; `
514
+ : 'No ledger rows yet - meta agent and ask spend is not counted; ') +
515
+ 'project agents for every run on disk.',
516
+ '',
517
+ `**${hum(total(grand))} tokens** in **${grand.calls} model calls**: ` +
518
+ [...totals.entries()].map(([a, s]) => `${a} ${pct(total(s), total(grand))}`).join(', ') +
519
+ '.',
520
+ '',
521
+ '## Totals',
522
+ '',
523
+ ...table(
524
+ ['who', ...HEAD],
525
+ [...totals.entries()]
526
+ .map(([a, s]) => [a, ...cells(s)])
527
+ .concat([['**all**', ...cells(grand)]]),
528
+ ),
529
+ '',
530
+ `Project agents ran for ${clock(runs.reduce((acc, r) => acc + r.durationMs, 0))} over ${runs.length} run(s).`,
531
+ '',
532
+ '## By model',
533
+ '',
534
+ ...table(
535
+ ['model', ...HEAD],
536
+ heaviest(overall).map(([m, s]) => [`\`${m}\``, ...cells(s)]),
537
+ ),
538
+ '',
539
+ ...[...actors.entries()]
540
+ .filter(([, models]) => models.size > 1 || actors.size > 1)
541
+ .flatMap(([actor, models]) => [
542
+ `### ${actor}`,
543
+ '',
544
+ ...table(
545
+ ['model', ...HEAD],
546
+ heaviest(models).map(([m, s]) => [`\`${m}\``, ...cells(s)]),
547
+ ),
548
+ '',
549
+ ]),
550
+ '## By batch',
551
+ '',
552
+ '"project" is the cases themselves, every run of the batch; "meta" is the meta agent while it',
553
+ 'waited on and graded those runs; "saving" is the last with-memory run against the last',
554
+ 'no-memory run.',
555
+ '',
556
+ ...table(
557
+ ['batch', 'runs (no mem / mem)', 'project', 'meta', 'ask', 'total', 'saving'],
558
+ [
559
+ ...(total(setup) > 0
560
+ ? [['setup', '-', '-', hum(total(setup)), '-', hum(total(setup)), '-']]
561
+ : []),
562
+ ...[...batches.values()].map((b) => [
563
+ b.label,
564
+ `${b.nomem.length} / ${b.mem.length}`,
565
+ hum(total(b.project)),
566
+ total(b.meta) > 0 ? hum(total(b.meta)) : '-',
567
+ b.asks > 0 ? `${hum(total(b.ask))} (${b.asks})` : '-',
568
+ hum(total(b.project) + total(b.meta) + total(b.ask)),
569
+ b.saving,
570
+ ]),
571
+ ],
572
+ ),
573
+ '',
574
+ '## By run',
575
+ '',
576
+ ...table(
577
+ [
578
+ 'run',
579
+ 'verdict',
580
+ 'cases',
581
+ 'calls',
582
+ 'input',
583
+ 'cached',
584
+ 'output',
585
+ 'reasoning',
586
+ 'project',
587
+ 'time',
588
+ 'meta',
589
+ 'asks',
590
+ ],
591
+ runs.map((r) => {
592
+ const side = onRun.get(r.name);
593
+ const meta = side ? plus({ ...side.meta }, side.waiting) : zero();
594
+ return [
595
+ r.name,
596
+ r.verdict || '-',
597
+ `${r.ok}/${r.items}`,
598
+ String(r.spend.calls),
599
+ hum(r.spend.input),
600
+ pct(r.spend.cached, r.spend.input),
601
+ hum(r.spend.output),
602
+ hum(r.spend.reasoning),
603
+ hum(total(r.spend)),
604
+ clock(r.durationMs),
605
+ total(meta) > 0 ? hum(total(meta)) : '-',
606
+ side?.asks ? `${side.asks} · ${hum(total(side.ask))}` : '-',
607
+ ];
608
+ }),
609
+ ),
610
+ '',
611
+ `## Costliest cases`,
612
+ '',
613
+ ...table(
614
+ ['run', 'case', 'calls', 'input', 'cached', 'output', 'total', 'time'],
615
+ [...cases]
616
+ .sort((a, b) => total(b.spend) - total(a.spend))
617
+ .slice(0, COSTLIEST)
618
+ .map((c) => [
619
+ c.run,
620
+ `\`${c.id}\``,
621
+ String(caseCalls.get(`${c.run}/${c.id}`) ?? '-'),
622
+ hum(c.spend.input),
623
+ pct(c.spend.cached, c.spend.input),
624
+ hum(c.spend.output),
625
+ hum(total(c.spend)),
626
+ clock(c.durationMs),
627
+ ]),
628
+ ),
629
+ '',
630
+ '## Meta agent',
631
+ '',
632
+ '### By stage',
633
+ '',
634
+ ...table(
635
+ ['stage', ...HEAD],
636
+ [...metaStages.entries()].map(([stage, s]) => [stage, ...cells(s)]),
637
+ ),
638
+ '',
639
+ '### Sessions',
640
+ '',
641
+ '"copilot says" is the session\'s own total, written when it exits; "calls seen" is the sum of',
642
+ 'the calls recorded while it ran. They should agree; a gap is calls made while nothing was',
643
+ 'recording.',
644
+ '',
645
+ ...table(
646
+ ['session', 'from', 'to', 'resumes', 'exit', 'calls seen', 'tokens seen', 'copilot says'],
647
+ [...sessions.values()]
648
+ .sort((a, b) => a.first.localeCompare(b.first))
649
+ .map((s) => [
650
+ `\`${s.session.slice(0, 8)}\``,
651
+ s.first.replace('T', ' ').slice(5, 16),
652
+ s.last.replace('T', ' ').slice(5, 16),
653
+ String(s.resumes),
654
+ String(s.exitCode),
655
+ String(s.spans.calls),
656
+ hum(total(s.spans)),
657
+ s.totals ? `${s.totals.calls} · ${hum(total(s.totals))}` : '-',
658
+ ]),
659
+ ),
660
+ '',
661
+ '## Not counted',
662
+ '',
663
+ '- embeddings: memory recall and commit, and `zen memory merge`',
664
+ '- `zen rag` searches the agents run inside the sandbox',
665
+ '- anything the meta agent did before the ledger started, and any meta agent not run',
666
+ ' through `zen meta`',
667
+ '',
668
+ ];
669
+
670
+ /** @param {Map<string, Spend>} map */
671
+ const plain = (map) => Object.fromEntries(heaviest(map));
672
+ const data = {
673
+ generatedAt: now,
674
+ ledgerSince: since ?? null,
675
+ totals: { all: grand, ...Object.fromEntries(totals) },
676
+ models: Object.fromEntries([...actors.entries()].map(([a, m]) => [a, plain(m)])),
677
+ batches: [...batches.values()].map((b) => ({
678
+ batch: b.label,
679
+ nomemRuns: b.nomem.length,
680
+ memRuns: b.mem.length,
681
+ project: b.project,
682
+ meta: b.meta,
683
+ ask: b.ask,
684
+ asks: b.asks,
685
+ saving: b.saving,
686
+ })),
687
+ runs: runs.map((r) => ({
688
+ run: r.name,
689
+ verdict: r.verdict,
690
+ items: r.items,
691
+ ok: r.ok,
692
+ durationMs: r.durationMs,
693
+ project: r.spend,
694
+ models: plain(r.models),
695
+ meta: onRun.get(r.name) ?? null,
696
+ })),
697
+ meta: {
698
+ stages: Object.fromEntries(metaStages),
699
+ sessions: [...sessions.values()],
700
+ },
701
+ };
702
+
703
+ if (toStdout) {
704
+ process.stdout.write(`${md.join('\n')}\n`);
705
+ process.exit(0);
706
+ }
707
+
708
+ /** @param {string} path @param {string} text */
709
+ function writeAtomic(path, text) {
710
+ mkdirSync(dirname(path), { recursive: true });
711
+ writeFileSync(`${path}.tmp`, text);
712
+ renameSync(`${path}.tmp`, path);
713
+ }
714
+ writeAtomic(REPORT, `${md.join('\n')}\n`);
715
+ writeAtomic(DATA, `${JSON.stringify(data, null, 2)}\n`);
716
+ if (!quiet) {
717
+ console.log(
718
+ `${NAME}: ${hum(total(grand))} tokens, ${grand.calls} calls - ` +
719
+ [...totals.entries()].map(([a, s]) => `${a} ${hum(total(s))}`).join(', ') +
720
+ ` - ${REPORT}`,
721
+ );
722
+ }