@zenera/cli 1.1.22 → 1.1.24

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 (73) hide show
  1. package/README.md +72 -2
  2. package/dist/archive.d.ts +139 -0
  3. package/dist/archive.js +376 -0
  4. package/dist/ask.d.ts +51 -0
  5. package/dist/ask.js +143 -0
  6. package/dist/batch.d.ts +77 -0
  7. package/dist/batch.js +338 -0
  8. package/dist/command.d.ts +6 -0
  9. package/dist/commands/export.d.ts +3 -0
  10. package/dist/commands/export.js +255 -0
  11. package/dist/commands/import.d.ts +3 -0
  12. package/dist/commands/import.js +173 -0
  13. package/dist/commands/index.d.ts +5 -1
  14. package/dist/commands/index.js +13 -1
  15. package/dist/commands/inspect.d.ts +11 -0
  16. package/dist/commands/inspect.js +388 -89
  17. package/dist/commands/list.js +128 -19
  18. package/dist/commands/memory.js +453 -16
  19. package/dist/commands/models.js +14 -2
  20. package/dist/commands/run.js +139 -34
  21. package/dist/commands/sandbox.js +72 -1
  22. package/dist/engine.d.ts +19 -0
  23. package/dist/engine.js +60 -8
  24. package/dist/liveness.js +6 -3
  25. package/dist/main.js +5 -2
  26. package/dist/podman.d.ts +16 -0
  27. package/dist/podman.js +115 -2
  28. package/dist/progress.d.ts +33 -0
  29. package/dist/progress.js +402 -0
  30. package/dist/request.d.ts +45 -0
  31. package/dist/request.js +300 -0
  32. package/dist/sandbox.js +10 -0
  33. package/dist/scaffold.d.ts +8 -0
  34. package/dist/scaffold.js +4 -1
  35. package/dist/session.d.ts +26 -1
  36. package/dist/session.js +42 -3
  37. package/dist/term.d.ts +3 -0
  38. package/dist/term.js +17 -1
  39. package/dist/trace.d.ts +84 -0
  40. package/dist/trace.js +686 -0
  41. package/dist/validate.js +93 -4
  42. package/package.json +12 -6
  43. package/templates/editor/.github/copilot-instructions.md +11 -6
  44. package/templates/editor/.github/prompts/finetune.prompt.md +251 -0
  45. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +25 -2
  46. package/templates/editor/.github/skills/zen-cli/SKILL.md +14 -3
  47. package/templates/editor/.github/skills/zen-cli/references/check.md +13 -5
  48. package/templates/editor/.github/skills/zen-cli/references/export.md +107 -0
  49. package/templates/editor/.github/skills/zen-cli/references/inspect.md +262 -21
  50. package/templates/editor/.github/skills/zen-cli/references/memory.md +123 -15
  51. package/templates/editor/.github/skills/zen-cli/references/models.md +1 -1
  52. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  53. package/templates/editor/.github/skills/zen-cli/references/rag.md +6 -0
  54. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  55. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1000 -0
  56. package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +432 -0
  57. package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +199 -0
  58. package/templates/editor/.github/skills/zen-inspect/SKILL.md +396 -0
  59. package/templates/editor/.github/skills/zen-memory/SKILL.md +74 -28
  60. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +370 -408
  61. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +19 -0
  62. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  63. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  64. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  65. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +6 -5
  66. package/templates/editor/.vscode/agents.schema.json +1 -1
  67. package/templates/editor/.vscode/settings.json +1 -0
  68. package/templates/project/SPECIFICATION.md.tmpl +2 -0
  69. package/templates/project/agents/files-instructions.md +13 -0
  70. package/templates/project/agents/fork-instructions.md +27 -0
  71. package/templates/project/agents/memory-instructions.md +80 -7
  72. package/templates/project/gitignore +3 -0
  73. package/templates/project/scripts/_setup.sh +49 -7
@@ -0,0 +1,1000 @@
1
+ ---
2
+ name: zen-finetune
3
+ description: Fine-tune an agent project against a training set in two stages — first WITHOUT memory, then WITH it. Turn a file of example queries in any format into a cached, classified, complexity-annotated dataset with rubrics; sample a batch uniformly across classes and complexities preferring graded samples. Stage 1 runs the batch cold with `zen run batch` against an empty memory, grades every trajectory with `zen inspect graph`, `zen inspect node` and `zen inspect ask` for rubric compliance, optimality, memory hygiene, fork use and delegation, generalises the findings into prompts, skills and new `agents/<topic>-policy-instructions.md` files, and re-runs cold until every query holds and what the runs write to memory is generalisable. Stage 2 then freezes the prose, merges the last good batch's memory, re-runs the same cases with every item given its own writable copy of that graph, and tunes the memory policy until recall makes the batch dramatically faster and cheaper without changing a verdict. Load before evaluating an agent project against example queries, before acting on "it gets this wrong", when asked to improve prompts or instructions from evidence rather than taste, when building an eval or regression set out of a specification, when optimising memory use or run cost, or whenever a batch of runs has to be graded rather than merely executed.
4
+ ---
5
+
6
+ # Fine-tuning an agent project
7
+
8
+ Nothing here touches model weights. What gets tuned is the prose the project is
9
+ made of — the agent prompts, the skills, the house rules in `agents/` — and what
10
+ it is tuned against is a dataset of real queries and the trajectories they
11
+ produce. The model is fixed; the instructions around it are the parameters.
12
+
13
+ The reason it is a loop rather than a review is that an instruction's effect is
14
+ not legible from reading it. A rule that reads as obvious gets ignored; a rule
15
+ nobody expected to matter turns out to be load-bearing. The only way to know
16
+ which is which is to run the queries, read the graphs, change one thing, and run
17
+ them again — which is what everything below is about.
18
+
19
+ It is two loops, not one: a first stage run entirely **without memory**, which
20
+ tunes the prose until the agent is correct from nothing, and a second stage run
21
+ **with memory**, which tunes recall until the same work costs a fraction of what
22
+ it did. They run in that order and never at the same time.
23
+
24
+ ```mermaid
25
+ flowchart TD
26
+ A[training set, any format] --> B[dataset.json<br/>classified · rated · rubrics]
27
+ B --> C[sample N<br/>uniform across strata]
28
+
29
+ subgraph S1["STAGE 1 — no memory: tune the prose"]
30
+ D[zen run batch<br/>cold, empty memory, still writes]
31
+ E[grade trajectories<br/>zen inspect]
32
+ F{every sample holds<br/>and writes generalise?}
33
+ G[generalise findings<br/>prompts · skills · house rules]
34
+ D --> E --> F
35
+ F -- no --> G
36
+ G --> D
37
+ end
38
+
39
+ subgraph S2["STAGE 2 — with memory: tune recall"]
40
+ H[merge memory from<br/>the last good cold batch]
41
+ I[same batch, each item<br/>its own writable copy]
42
+ J{shorter, still correct,<br/>and did it re-commit?}
43
+ K[generalise findings<br/>memory-policy-instructions.md]
44
+ H --> I --> J
45
+ J -- no --> K
46
+ K --> I
47
+ end
48
+
49
+ C --> D
50
+ F -- yes --> H
51
+ J -- yes --> L[done: commit dataset,<br/>findings and changes]
52
+ ```
53
+
54
+ The dataset is built once and cached. Everything after it runs many times.
55
+
56
+ ## The two stages
57
+
58
+ Fine-tuning is two stages, run in order and never interleaved. They optimise
59
+ different things, they are graded on different evidence, and running them
60
+ together makes both unreadable.
61
+
62
+ | | **Stage 1 — without memory** | **Stage 2 — with memory** |
63
+ | ------------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
64
+ | Phases | 1 to 7 | 8 |
65
+ | What is tuned | agent prompts, skills, `agents/instructions.md`, new policy files | the memory policy: what is committed, and when it is recalled |
66
+ | Memory at run time | `--memory .finetune/empty` — every item starts empty and **writes its own graph** | `--memory <merged>` — every item starts from **its own copy** of one graph and writes into that |
67
+ | Graded on | the eight criteria; the trajectory must be right _from nothing_ | whether recall shortened the trajectory, and what was committed on top of a graph that knew |
68
+ | The win | correctness — the agent does the right work for the right reason | speed and cost — the same verdicts for dramatically fewer tokens and less wall clock |
69
+ | Done when | every sample holds cold, and what each run _wrote_ to memory is generalisable | every sample still holds warm, materially cheaper, and no recall replaced a live reading |
70
+
71
+ **Both stages run the same tool surface, and that is the point.** A batch given
72
+ a memory directory copies it per item and lets the item write — cold from an
73
+ empty directory, warm from the merged graph — so the agent sees the same tools
74
+ and the same house rules in both. `--memory-read-only` would share one graph
75
+ across the batch instead, but it also clamps every agent's `access` to `read`,
76
+ which removes `memory_commit` from the schema and changes the memory prose the
77
+ model reads. That is two variables moving at once, and a warm round has only one
78
+ number to explain them with. Keep the flag for production-shaped runs where many
79
+ processes genuinely share one graph; do not tune with it.
80
+
81
+ **Stage 1 writes memory but never reads it.** That is not a side effect to be
82
+ tolerated — it is half of what stage 1 grades. Each cold item commits into its
83
+ own empty graph, so the commits are a clean record of what that run _thought was
84
+ worth keeping_, uncontaminated by anything a previous run left behind. Criterion
85
+ 3 grades exactly that: a run that cached today's inbox as a fact has poisoned
86
+ every future run, and a run that learned a new API's shape and committed nothing
87
+ has thrown the round away. Fixing those is stage 1 work, because the merged graph
88
+ stage 2 measures against is built out of these commits. Tuning recall on top of
89
+ commits you have not graded is tuning on sand.
90
+
91
+ **Stage 2 never reads a cold run's prose problem as a memory problem.** If a
92
+ sample is still failing cold, memory can hide it — the warm run passes, the graph
93
+ is rebuilt one day, and the failure returns with no trace of why. So stage 2 does
94
+ not begin until stage 1 is green. When it does begin, the prompts, skills and
95
+ house rules are frozen: the only artefact stage 2 edits is
96
+ `agents/memory-policy-instructions.md`. Changing both layers at once gives two
97
+ causes for one number.
98
+
99
+ The expected shape of a successful stage 2 is a large drop, not a small one.
100
+ Cold, every run rediscovers the same endpoints, the same schema, the same layout
101
+ of the same system, every time. Warm, it should recall them and go straight to
102
+ work — fewer llm calls, fewer tool calls, less wall clock, same verdicts. A warm
103
+ round that is only a few percent cheaper has not used memory; it has merely
104
+ carried it.
105
+
106
+ ## What fine-tuning may change
107
+
108
+ | Artefact | Stage | Changed by this loop |
109
+ | --------------------------------------------------- | ------ | --------------------------------------------------------------------- |
110
+ | `agents/prompts/<name>.md` | 1 | Yes — the usual place a per-agent finding lands |
111
+ | `agents/skills/<name>/SKILL.md` | 1 | Yes — when the fix is knowledge, not standing policy |
112
+ | `agents/instructions.md` | 1 | Yes — when the fix binds every agent in the project |
113
+ | `agents/<topic>-policy-instructions.md` | 1 | Yes — new files are created here, one topic per file |
114
+ | `agents/memory-policy-instructions.md` | 1 or 2 | Yes — commit policy in stage 1, recall policy in stage 2 |
115
+ | `agents.yaml` | 1 | Rarely — only when the finding is structural (wrong tools, wrong fan) |
116
+ | `agents/memory-instructions.md` and the other three | — | **Never.** They are `zen`'s copies; `zen check --fix` overwrites them |
117
+ | The dataset | — | Only to add samples or fix a wrong rubric — never to make a run pass |
118
+
119
+ Moving a failing sample's rubric to match what the agent did is not tuning. It
120
+ is deleting the test. If a rubric was wrong, say so in the round's notes and fix
121
+ it before the round, not after seeing the result.
122
+
123
+ ## Phase 1 — Build the dataset (shared)
124
+
125
+ ### The input is whatever the user has
126
+
127
+ A training set arrives as a section of `SPECIFICATION.md`, a markdown file of
128
+ headed examples, a transcript, a spreadsheet exported to CSV, a folder of
129
+ screenshots with captions. There is no format to parse, so do not try to write a
130
+ parser. Read the file, understand which parts are queries, and write out the
131
+ structure yourself. This is the one step in the loop that is a model's job and
132
+ not a script's.
133
+
134
+ What to look for:
135
+
136
+ | In the source | In the dataset |
137
+ | ---------------------------------------------------------------- | -------------------------------------------------------------------- |
138
+ | A heading like `## Query:` and a quoted string | One sample, `input` is the string |
139
+ | A bullet list under `### Rubric:` | `rubric` — **verbatim**, one entry per bullet |
140
+ | An embedded image, a `![](...)` link, an audio or PDF attachment | A media part in `input`, path relative to the dataset file |
141
+ | A paragraph of context before the query | `notes`, not `input` — unless the agent is meant to see it |
142
+ | The expected answer | `expected`, one string; it is context for grading, not a diff target |
143
+ | Nothing but a query | A sample with no `rubric`. Still useful, ranked lower |
144
+
145
+ Preserve the rubric word for word. A paraphrased rubric grades the paraphrase.
146
+
147
+ ### The shape
148
+
149
+ Write `.finetune/dataset.json`:
150
+
151
+ ```json
152
+ {
153
+ "version": 1,
154
+ "source": ["SPECIFICATION.md", "docs/examples/day-planning.md"],
155
+ "extractedAt": "2026-03-04T10:12:00Z",
156
+ "samples": [
157
+ {
158
+ "id": "planning-organize-day",
159
+ "class": "planning",
160
+ "complexity": "medium",
161
+ "input": "organize my day",
162
+ "rubric": [
163
+ "calls api /mail/list",
164
+ "calls api /calendar/list for today",
165
+ "correlates emails with calendar events",
166
+ "proposes a schedule, does not just list both"
167
+ ],
168
+ "expected": "A time-ordered plan naming the two conflicting meetings.",
169
+ "notes": "From SPECIFICATION.md, worked example."
170
+ },
171
+ {
172
+ "id": "receipts-photo-expense",
173
+ "class": "extraction",
174
+ "complexity": "simple",
175
+ "input": ["file this expense", { "image": "./assets/receipt-014.jpg" }],
176
+ "rubric": ["reads the total off the image", "does not invent a vendor"]
177
+ }
178
+ ]
179
+ }
180
+ ```
181
+
182
+ `input` is exactly what `zen run batch` accepts: a string, or an array of parts.
183
+ A part is a string, `{ "text": "…" }`, or `{ "image" | "audio" | "video" | "file": "<path or url>" }`
184
+ with an optional `"mimeType"`. Relative paths resolve against the file they are
185
+ written in — so keep the generated `cases.json` in the same directory tree as the
186
+ media, or write absolute paths. Local media is inlined as base64 and capped at
187
+ 20 MB per part, so reference a URL for anything large.
188
+
189
+ `id` names a directory under the batch dir. Letters, digits, dot, dash and
190
+ underscore only, unique across the dataset, and worth making readable:
191
+ `class-short-slug` means the batch directory listing is already an index.
192
+
193
+ ### Classify only what the source already tells you
194
+
195
+ The instruction is "if obvious from the input" and it should be taken literally.
196
+ A class is a distinction the source makes — by section heading, by which agent or
197
+ API the example is about, by task type. Do not invent a taxonomy; six classes
198
+ over forty samples is useful, twenty classes over forty samples is one sample per
199
+ class and stratified sampling degenerates into taking everything. When a sample
200
+ fits nowhere, leave `class` out: the sampler groups those as `unclassified` and
201
+ they still get their share.
202
+
203
+ ### Annotate complexity
204
+
205
+ Three levels, and rate the _trajectory_ the query demands, not the sentence:
206
+
207
+ | Level | What it means |
208
+ | --------- | --------------------------------------------------------------------------- |
209
+ | `simple` | One tool call or none; one agent; the answer is a lookup or a restatement |
210
+ | `medium` | Several calls that depend on each other, or one delegation, or one artefact |
211
+ | `complex` | Fan-out, multi-agent, ambiguity to resolve, or a plan that can go wrong |
212
+
213
+ "organize my day" is three words and complex. Complexity is where the interesting
214
+ failures are, which is why the sampler spreads across it rather than letting the
215
+ easy majority set the score.
216
+
217
+ ### It is a cache
218
+
219
+ Extraction is expensive and non-deterministic; re-doing it per round would make
220
+ two rounds incomparable. So `.finetune/dataset.json` is written once and read
221
+ thereafter. Re-extract only when the source files change, and when you do, keep
222
+ the existing ids stable — an id that moves breaks every previous round's
223
+ findings. Commit the dataset. It is the project's eval set from then on.
224
+
225
+ ## Phase 2 — Sample (shared)
226
+
227
+ Input is limited: a batch of 100 complex runs is a lot of tokens and a long wait,
228
+ and nothing is learned from the eightieth one that the twentieth did not already
229
+ say. Pick a budget — 10 to 25 is a working round — and spend it evenly.
230
+
231
+ ```sh
232
+ .github/skills/zen-finetune/scripts/sample.sh -n 16 -o .finetune/rounds/r1/cases.json
233
+ ```
234
+
235
+ | Flag | Effect |
236
+ | ---------------------- | --------------------------------------------------------- |
237
+ | `-n <N>` | Budget. Without it, everything, still in stratified order |
238
+ | `--class <name>` | One class only — for a targeted round after a class fails |
239
+ | `--complexity <level>` | One level only |
240
+ | `--rubric-only` | Drop samples with no rubric |
241
+ | `--seed <n>` | Deterministic. The same seed picks the same batch |
242
+ | `-o <file>` | Where to write; default stdout |
243
+
244
+ The sampler groups the dataset into strata of one class × one complexity, sorts
245
+ rubric-bearing samples to the front of each, and then takes one from each stratum
246
+ in turn until the budget runs out. Three consequences worth knowing:
247
+
248
+ - A budget smaller than the dataset still buys a cross-section of it, not the
249
+ front of the file.
250
+ - Rare classes are over-represented relative to their share. That is deliberate.
251
+ A class with two samples is the one most likely to be under-instructed.
252
+ - Graded samples come first, so a small budget is spent almost entirely on
253
+ samples that can say _why_ they failed rather than only _what_ happened.
254
+
255
+ Keep the seed fixed across the rounds of one tuning session. Changing the sample
256
+ and the instructions at the same time means a difference in the score has two
257
+ possible causes and you cannot tell which.
258
+
259
+ ## Phase 3 — Stage 1: run cold
260
+
261
+ **Check the machine before the first cold batch, and again after any change to
262
+ `--concurrency`, to the indexes, or to the podman VM:**
263
+
264
+ ```sh
265
+ .github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh --concurrency 8 \
266
+ && zen run batch …
267
+ ```
268
+
269
+ It takes seconds, starts nothing you have to clean up, and exits non-zero when
270
+ the batch cannot fit. Skipping it is how five rounds get spent grading prose
271
+ that was never the cause. Load `zen-sandbox-capacity` if it refuses.
272
+
273
+ ```sh
274
+ cd <project>
275
+ mkdir -p .finetune/rounds/r1
276
+ zen run batch \
277
+ --input .finetune/rounds/r1/cases.json \
278
+ --batch-dir .finetune/rounds/r1/batch \
279
+ --memory .finetune/empty \
280
+ --concurrency 8
281
+ ```
282
+
283
+ `--memory` naming a directory that does not exist is how a batch is made cold:
284
+ each item gets its own empty graph, the project's real `memory/` is not read and
285
+ not touched, and every item still writes, so what it _chose_ to remember is
286
+ evidence. Both halves matter, and both are graded in this stage: the trajectory
287
+ has to be right starting from nothing, and the commits have to be worth the
288
+ next run's attention. Never point a tuning batch at the project's live memory —
289
+ a warm graph means a run can succeed by recall instead of by instruction, which
290
+ is precisely the thing stage 1 exists to rule out.
291
+
292
+ Concurrency has two ceilings and the provider is only one of them. The default
293
+ is 16 and the cap is 32, but rate-limit errors turn a graded run into a retried
294
+ one — and the machine's ceiling is usually the lower of the two, because every
295
+ item is a container and the podman VM is not the host. Whichever binds first,
296
+ `--concurrency` is the dial; see `zen-sandbox-capacity` for the other ceiling.
297
+ `stdout` is the batch directory alone, so `BATCH="$(zen run batch …)"` is safe
298
+ to script. `<batch-dir>/README.md` is a live dashboard — open it while the
299
+ batch runs.
300
+
301
+ What lands:
302
+
303
+ ```
304
+ .finetune/rounds/r1/batch/
305
+ batch.json roster, counts, timings, memory mode, tokens per model
306
+ README.md the dashboard
307
+ <id>/output.json the envelope: run.dir, run.graph, usage, output
308
+ <id>/workspace/ anything the item wrote
309
+ <id>/memory/ what the item committed, if it committed anything
310
+ ```
311
+
312
+ What a round cost is in both: `README.md` draws a `Tokens by model` table, and
313
+ `batch.json` carries the same split under `batch.models`. Read it per model — a
314
+ round that moved work onto a cheaper agent shows up there and nowhere else.
315
+
316
+ An item that fails is data, not an outage: its `output.json` holds
317
+ `{ "ok": false, "id", "error" }` and the rest of the batch still runs. Grade the
318
+ failures too — a crash is a finding.
319
+
320
+ ## Phase 4 — Stage 1: grade the trajectories
321
+
322
+ ```sh
323
+ .github/skills/zen-finetune/scripts/collect.sh -d .finetune/rounds/r1/batch
324
+ .github/skills/zen-finetune/scripts/collect.sh -d .finetune/rounds/r1/batch graphs > /tmp/r1.mmd
325
+ ```
326
+
327
+ `collect.sh index` prints one line per item — verdict, agent, stop reason,
328
+ tokens, duration, whether it has a rubric, and its run directory.
329
+ `collect.sh graphs` concatenates every item's `graph.mmd` with its id and rubric
330
+ as a header, which is one read instead of N invocations of `zen inspect graph`.
331
+ `collect.sh compare` is the per-sample cost table the round is written up with,
332
+ and it is the last thing this phase does rather than the first — read the
333
+ trajectories before the numbers, or the numbers decide what you look at.
334
+
335
+ Then work item by item. Load **zen-inspect** for the mechanics; this is what to
336
+ look for.
337
+
338
+ ### The three depths
339
+
340
+ | Depth | Answers | Cost |
341
+ | --------------------------------------------------------------- | ------------------------------------------------- | -------------- |
342
+ | The graph (`graph.mmd`, or `zen inspect graph --dir <run.dir>`) | What happened, in what order, and where it looped | Free |
343
+ | `zen inspect node n13 --dir <run.dir>` | What a step actually said, sent or got back | Free |
344
+ | `zen inspect ask <llm_call-id> "…"` | Why the model chose this over that | One model call |
345
+
346
+ Stay at the top for as long as it works. The `%%` header rows on the graph carry
347
+ `nodes`, `tokens`, `thinking`, `tools`, `branches`, `compacted` and `why` — and
348
+ the `tools` row is a loop detector: the same tool with the same arguments three
349
+ times is a finding without opening a single node.
350
+
351
+ Descend to `node` for evidence. A finding that does not cite node ids is an
352
+ impression, and impressions are how a tuning round produces a rule nobody needed.
353
+
354
+ `ask` is last, and it is worth the call in exactly one situation: the graph shows
355
+ a choice you cannot explain and the node contents do not explain it either. It
356
+ replays one `llm_call` with the same context, no tools, and writes nothing back —
357
+ so the answer is that call's own reasoning, not a reconstruction.
358
+
359
+ ```sh
360
+ RUN="$(jq -r .run.dir .finetune/rounds/r1/batch/planning-organize-day/output.json)"
361
+ zen inspect graph --dir "$RUN"
362
+ zen inspect node n12 n13 --dir "$RUN" --part request --full
363
+ zen inspect ask n13 "why did you call /mail/list twice instead of paging the first result?" --dir "$RUN"
364
+ ```
365
+
366
+ Treat the answer as testimony, not ground truth. It is the model's account of the
367
+ model's choice; it is useful because it names which instruction it was reading,
368
+ and that is the instruction you are about to change.
369
+
370
+ ### The criteria
371
+
372
+ Grade each sample against all of these. The first is the user's; the rest apply
373
+ whether or not a rubric was written.
374
+
375
+ **1. Rubric compliance.** Every line of the rubric, checked against the graph.
376
+ Mark each `met` / `partial` / `missed` and cite the node. A rubric line naming an
377
+ API call is met by the call appearing with sensible arguments — not by the answer
378
+ claiming it was made.
379
+
380
+ **2. Optimality.** Did it take the short path? Symptoms, all visible in the
381
+ graph: the same read twice; a search that could have been one call made as four;
382
+ a tool loop; thinking tokens spent re-deriving something already in context; a
383
+ compaction that a shorter trajectory would not have needed. Record `nodes` and
384
+ `tokens` from the header — they are the round-over-round number.
385
+
386
+ **3. Memory hygiene.** Read `<id>/memory/` and the commit nodes. The rule is
387
+ commit what _produces_ the answer, not the material it was made from: a file
388
+ becomes a pointer, a call becomes the operation. So a run that stored the
389
+ _contents_ of `/mail/list` — today's inbox, a price, a status, a queue depth —
390
+ has cached a live reading as if it were a fact, and every later run is wrong.
391
+ What it should have stored is that the mail list is at that endpoint, what shape
392
+ it returns and what the useful filter was. The carve-outs are narrow: an answer
393
+ the agent assembled itself, and a source that genuinely cannot change. Also
394
+ check the other direction — a run that committed nothing at all when it learned
395
+ the shape of a new API has thrown the round away.
396
+
397
+ **4. Fork.** Independent work should fan out; dependent work must not. Look for:
398
+ serial calls with no data dependency between them (missed fork); branches that
399
+ read each other's output (a fork that should have been a sequence); a fork with
400
+ one branch (that is delegation, and should be written as delegation); branches
401
+ that all do the same thing with the same inputs. `branches` in the header and the
402
+ shape of the graph say all of it.
403
+
404
+ **5. Delegation.** Did it hand off to the right agent, and did it hand off at
405
+ all? Both failures are common: doing a specialist's job inline because the prompt
406
+ never said to delegate, and delegating a one-line question because the prompt
407
+ said to delegate too eagerly. Check the handoff carries enough context to be
408
+ answerable, and that the result comes back somewhere rather than ending the run.
409
+
410
+ **6. Tools and skills.** Did it use the tool it was given, or hand-roll the same
411
+ thing in a shell? Did it load the skill that covers the task — and if it loaded
412
+ it, did it then follow it? A skill loaded and ignored is a prose problem in the
413
+ skill, not a routing problem.
414
+
415
+ **7. Grounding.** Is every claim in the final answer traceable to something the
416
+ graph shows it read? An answer that is right by luck grades as a failure; it will
417
+ be wrong by luck next time.
418
+
419
+ **8. Failure handling.** When a tool returned an error, did it adapt, or did it
420
+ retry the identical call? The `tools` header row finds this in one line.
421
+
422
+ ### Write it down
423
+
424
+ One file per round, `.finetune/rounds/r1/findings.md`. It opens with a **verdict
425
+ block** — a plain-language answer to "are we making progress", what was changed
426
+ to get there, and the per-sample table that either supports that or does not —
427
+ and only then the spec and the per-sample grading.
428
+
429
+ Write the verdict block last and put it first. Somebody who has not read the
430
+ earlier rounds should learn from the top of the page whether the work is moving;
431
+ somebody who has should be able to stop after it.
432
+
433
+ The table is not typed by hand. `compare` builds it against the previous round
434
+ and prints markdown ready to paste:
435
+
436
+ ```sh
437
+ .github/skills/zen-finetune/scripts/collect.sh \
438
+ -d .finetune/rounds/r2/batch -p .finetune/rounds/r1/batch compare
439
+ ```
440
+
441
+ Every column comes from the run itself — tokens from `output.json`, llm calls,
442
+ tool calls and forks from the graph's own `%% nodes` header row, time from
443
+ `durationMs`. The one column it leaves as `?` is **verdict**, because whether a
444
+ sample is right is the judgement the previous phase made and no script can read
445
+ it off a trajectory. Fill that column in from the rubric grading below.
446
+
447
+ Above the table it prints a `memory:` line for this round and, when comparing, a
448
+ `prev memory:` line for the other. Both belong in `findings.md` as printed. They
449
+ are not a footnote: the same numbers mean opposite things cold and warm, and a
450
+ round whose report does not say which cannot be compared to any other round
451
+ later. `zen run batch --memory .finetune/empty` records in `batch.json` as mode
452
+ `copied` exactly like a warm run does, so do not read the mode out of the JSON
453
+ by eye — `collect.sh` decides it by whether the source is a real graph.
454
+
455
+ ```md
456
+ # r2 — COLD, --concurrency 7
457
+
458
+ ## PROGRESS — one sample fixed, none broken
459
+
460
+ memory: COLD — every item started from an empty graph and wrote its own (.finetune/empty)
461
+ prev memory: COLD — every item started from an empty graph and wrote its own (.finetune/empty)
462
+
463
+ **Changed since r1:** one paragraph in `agents/skills/rag_search/SKILL.md`
464
+ (+11 lines) — a question naming two products is two lookups, and the worst
465
+ verdict decides.
466
+
467
+ | sample | verdict | tokens | llm calls | tool calls | forks | time |
468
+ | -------------------------------- | ------- | ------------------------ | --------- | ---------- | ------- | ------------------ |
469
+ | info-upgrade-pair-4100-4110 | ✘ → ✔ | 923k → 2.04M (+121%) | 23 → 49 | 24 → 47 | 1 (=) | 74s → 128s (+73%) |
470
+ | info-intel-4120-compat-420 | ✔ → ✔ | 676k → 1.09M (+61%) | 24 → 33 | 21 → 30 | 1 (=) | 70s → 79s (+13%) |
471
+ | info-malware-profiles-medium-412 | ✘ → ✘ | 3.40M → 1.00M (−70%) | 65 → 41 | 61 → 36 | 2 (=) | 222s → 107s (−52%) |
472
+ | code-can-vms-talk | ✔ → ✔ | 1.20M → 4.80M (+302%) | 35 → 124 | 32 → 113 | 1 → 5 | 93s → 463s (+398%) |
473
+ | **total** | 14 → 15 | **19.7M → 24.3M (+23%)** | 585 → 717 | 517 → 637 | 27 → 31 | 1602s → 1922s |
474
+
475
+ **Effect:** `info-upgrade-pair-4100-4110` is fixed and nothing regressed. The
476
+ +121% on it is the second lookup the rule asks for, which is the cost of the
477
+ fix and not a surprise.
478
+
479
+ **Not the rule's doing:** `code-can-vms-talk` alone is +3.6 M of the +4.5 M, and
480
+ 124 llm calls against 35 is a loop, not variance — the rule cannot reach that
481
+ sample. Excluding it the round moves +5%, inside the noise floor.
482
+
483
+ **Verdict:** keep. One sample fixed for eleven lines. Open the loop next round.
484
+
485
+ machine: vm 12.0 GiB / 6 cpus · container 4096 MiB · index docs 3.2 GiB · host 36 GiB
486
+
487
+ ## planning-organize-day — partial
488
+
489
+ | # | Rubric | Verdict | Evidence |
490
+ | --- | --------------------------------------- | ------- | ----------------------------- |
491
+ | 1 | calls api /mail/list | met | n7 |
492
+ | 2 | calls api /calendar/list for today | met | n9 |
493
+ | 3 | correlates emails with calendar events | partial | n14 — matched on subject only |
494
+ | 4 | proposes a schedule, does not just list | missed | n21 |
495
+
496
+ - optimality: 34 nodes, 61k tokens. n7 and n11 are the same /mail/list call (2)
497
+ - memory: committed the body of the 09:00 stand-up invite as a fact (3) — n18
498
+ - fork: n9 and n7 are independent and ran in sequence (4)
499
+ - delegation: none needed
500
+ ```
501
+
502
+ Cite the criterion number. The next phase reads down the column.
503
+
504
+ The heading carries the verdict in one word, so a folder of rounds shows the
505
+ shape of the work without anything being opened. The round's own title carries
506
+ `COLD` or `WARM` for the same reason — `# r3 — WARM, --concurrency 8`. Use
507
+ exactly one of:
508
+
509
+ | Heading | Means |
510
+ | ------------ | ------------------------------------------------------------------ |
511
+ | `PROGRESS` | at least one sample fixed, none broken |
512
+ | `NO CHANGE` | the round ran clean and moved nothing — the change did not work |
513
+ | `REGRESSION` | a sample that passed now fails, whatever else improved |
514
+ | `MIXED` | something fixed and something broken in the same round |
515
+ | `VOID` | any exit 137, or the machine spec changed — nothing here is graded |
516
+ | `VERIFIED` | a confirmation round after both stages ended — nothing was changed |
517
+
518
+ Five rules keep the block honest, and each is a round that was wasted once:
519
+
520
+ - **Say whether memory was used, in the heading and in the `memory:` line.** A
521
+ warm round graded as a cold one reads as an agent that knew things it was
522
+ never told, and a cold round compared against a warm one attributes to a
523
+ prompt edit what was really recall. Both lines are printed by `compare`; paste
524
+ them. If `compare` warns that the two rounds ran on different memory, either
525
+ that _is_ the measurement (phase 8) and the heading must say so, or the wrong
526
+ `-p` was given.
527
+ - **The verdict column decides, not the token column.** Tokens swing ±2× between
528
+ identical runs of this kind of project. Say what noise floor you measured and
529
+ treat anything under it as unchanged — an aggregate move of 20% is usually
530
+ nothing.
531
+ - **Localise an aggregate change before attributing it to the edit.** That is
532
+ what the per-sample rows are for. Growth concentrated in the samples the rule
533
+ touches is the price of the rule; growth spread evenly across all of them is
534
+ prose leaking into every agent, which is a regression even when every sample
535
+ still passes.
536
+ - **Read the call columns, not only the tokens.** They are the ones that say
537
+ _why_: tool calls up with tokens is more work, tokens up with tool calls flat
538
+ is a longer prefix, and llm calls at three times the round before is a loop
539
+ that no amount of token arithmetic will name.
540
+ - **Name the fixed and broken samples.** "14 → 15" is not actionable and
541
+ `info-upgrade-pair-4100-4110` is. A round that fixes one and breaks one is
542
+ `MIXED`, not a draw — the broken one is the more valuable finding, because it
543
+ is almost always the new instruction.
544
+
545
+ `Verdict:` ends in keep or revert, and the ratio to judge it by is prose bought
546
+ per sample fixed. A round that added forty lines and fixed nothing is reverted,
547
+ not built on.
548
+
549
+ For a **warm** round (phase 8) the same command compares against the cold round
550
+ the memory was merged from, and the heading answers the only question that phase
551
+ asks — whether memory shortened the work:
552
+
553
+ ```sh
554
+ .github/skills/zen-finetune/scripts/collect.sh \
555
+ -d .finetune/rounds/r3/batch -p .finetune/rounds/r2/batch compare
556
+ ```
557
+
558
+ A warm round that costs _more_ than its cold round is `NO CHANGE` at best:
559
+ memory is being loaded and not used, and the fix is an instruction about when to
560
+ recall. A warm round that costs dramatically less has one thing left to check
561
+ before it counts — that the saving is recall replacing _discovery_ and not
562
+ recall replacing a live read. Open a sample's trajectory and confirm the calls
563
+ that must hit the live system still did.
564
+
565
+ The `machine:` line is not decoration. Token counts and wall clocks are only
566
+ comparable between rounds **measured on the same spec** — a resized VM
567
+ invalidates a baseline exactly as a prompt edit does, and a round with no spec
568
+ recorded cannot be compared to anything later. `preflight_sandbox.sh --json`
569
+ prints every figure on that line.
570
+
571
+ ### A round with any exit 137 is void, not graded
572
+
573
+ Before grading anything, check how the round died:
574
+
575
+ ```sh
576
+ .github/skills/zen-finetune/scripts/collect.sh <batch-dir> oom
577
+ ```
578
+
579
+ An item killed at 137 ran out of memory; an item at 124 hit a timeout, which
580
+ under memory pressure is the same fault wearing a different number. Neither
581
+ produced evidence about the prompt. **Do not grade the round, do not change one
582
+ word of instruction on its basis, and do not compare its tokens to anything** —
583
+ fix the machine, then run it again. A round scored on OOM debris will send the
584
+ next round after a defect that does not exist.
585
+
586
+ `failures` will not catch this and neither will the dashboard. An item whose
587
+ command was killed usually recovers, answers anyway and is recorded `ok`; a
588
+ round can read `16 items, 16 ok, 0 failed` with six of them OOM-killed inside.
589
+ The kill is in the graph, not in the verdict, which is why this is a separate
590
+ mode and why it runs before grading rather than after.
591
+
592
+ ## Phase 5 — Stage 1: generalise
593
+
594
+ This is where a tuning round is won or lost. Collect every finding from every
595
+ item into one table before writing a single word of instruction, because the
596
+ unit of change is not a finding — it is a _pattern across findings_.
597
+
598
+ **One item is an anecdote.** A rule written from a single sample's failure fixes
599
+ that sample and adds a line every other run has to read. Three items missing the
600
+ same beat is a rule. Two items and a plausible mechanism is a judgement call.
601
+ One item is a note in `findings.md` and nothing else, unless the failure was
602
+ catastrophic.
603
+
604
+ Then, for each pattern, ask what the _generalisable_ form is. "Call
605
+ `/calendar/list` before answering a scheduling question" is not it — that is the
606
+ dataset written into the prompt, and it will not transfer to the next query. The
607
+ generalisable form is one level up: "when a request depends on the current state
608
+ of an external system, read that state before proposing anything that acts on
609
+ it." Same fix, and it covers the queries not in the dataset.
610
+
611
+ Two traps, both of which have cost whole rounds:
612
+
613
+ - **A prohibition with a self-judged exemption is a permission.** "Do not do X,
614
+ except in rare cases where it is clearly warranted" is read by the model as
615
+ "do X when you want to", because every case is clearly warranted from inside
616
+ it. Either forbid it or do not.
617
+ - **Before rewriting a rule the agent keeps ignoring, grep the whole assembled
618
+ prefix for the rule's own text.** House rules, the prompt and every preloaded
619
+ skill are concatenated. A rule that is obeyed nowhere usually has a second,
620
+ softer copy somewhere the agent reads more often. The contradiction is the
621
+ bug; better adjectives are not the fix.
622
+
623
+ Prefer mechanical wording. Rules of the form "put X before Y", "if the path does
624
+ not end in `.py`, stop", "copy this literal verbatim" are followed. Rules that
625
+ require judgement — "be thorough", "generalise appropriately" — are followed
626
+ about half the time and are not worth their tokens.
627
+
628
+ ## Phase 6 — Stage 1: apply
629
+
630
+ Route each generalised rule by what kind of thing it is.
631
+
632
+ | The finding is… | It goes in |
633
+ | --------------------------------------------- | --------------------------------------------------- |
634
+ | One agent's behaviour, one agent's job | `agents/prompts/<agent>.md` |
635
+ | A fact, a format, an API shape, a procedure | A skill — extend one, or create one |
636
+ | A standing rule every agent must obey | `agents/instructions.md` |
637
+ | A standing rule about one capability | `agents/<topic>-policy-instructions.md` — new file |
638
+ | The wrong agent had the tool, or no agent did | `agents.yaml`, and say so loudly — it is structural |
639
+ | The dataset was wrong | `.finetune/dataset.json`, before the next round |
640
+
641
+ New policy files are the usual output of a first round: `memory-policy-`,
642
+ `fork-policy-`, `tools-policy-`, `files-policy-instructions.md`. They sit beside
643
+ `zen`'s own four, they are yours, and `zen check --fix` does not touch them. Load
644
+ **zen-instructions** before writing one — in particular for two things it is easy
645
+ to get wrong: `requires:` is _not_ inherited, so a policy file must repeat the
646
+ condition of the file it sits next to (`requires: memory` beside
647
+ `memory-instructions.md`), and the files are prepended in filename order, so
648
+ `memory-policy-` reads after `memory-instructions.md`, which is what you want.
649
+
650
+ Never edit `agents/memory-instructions.md`, `agents/fork-instructions.md`,
651
+ `agents/tools-instructions.md` or `agents/files-instructions.md`. They are copies
652
+ of the runtime templates; `zen check` reports them stale when they drift and
653
+ `zen check --fix` replaces them wholesale, taking your edit with it. If a finding
654
+ genuinely belongs in one of those — that is, it is true of every Zenera project
655
+ and not just this one — change the master upstream in the CLI templates instead,
656
+ and note it in the round.
657
+
658
+ Record what changed and why in `.finetune/rounds/r1/changes.md`, one entry per
659
+ edit, each naming the finding numbers that motivated it. That file is what makes
660
+ the next round's diff interpretable, and it is what you read when a score goes
661
+ _down_.
662
+
663
+ Then, before re-running:
664
+
665
+ ```sh
666
+ zen check
667
+ .github/skills/zen-review/scripts/check-paths.sh
668
+ ```
669
+
670
+ A broken project fails every item for one reason, and a round spent discovering
671
+ that is a round wasted.
672
+
673
+ ## Phase 7 — Stage 1: re-run cold, and leave the stage
674
+
675
+ Same cases file, same seed, new round directory, still no memory.
676
+
677
+ ```sh
678
+ mkdir -p .finetune/rounds/r2
679
+ zen run batch --input .finetune/rounds/r1/cases.json \
680
+ --batch-dir .finetune/rounds/r2/batch --memory .finetune/empty --concurrency 8
681
+ ```
682
+
683
+ Re-use `r1/cases.json` rather than re-sampling — the point of the round is the
684
+ difference, and the difference only exists if the queries are identical. Compare
685
+ per sample and on the aggregate: verdicts, nodes, tokens.
686
+
687
+ Three things to watch for:
688
+
689
+ - **Regression.** A sample that passed in r1 and fails in r2 is the most valuable
690
+ result in the round, and it is almost always the new instruction. Prose added
691
+ for one agent is read by all of them.
692
+ - **The score going up for the wrong reason.** If the new rule names the
693
+ dataset's own endpoints or entities, the batch passes and nothing generalises.
694
+ Sample a query the dataset does not contain and try it by hand.
695
+ - **Prose growth.** If the instruction files have grown by a third and the score
696
+ moved by one sample, the round was net negative. Delete something.
697
+
698
+ Loop until every sample in the batch behaves. Then widen: sample a _larger_ N
699
+ from the dataset with the same seed — the earlier items recur, so the previous
700
+ round's samples act as a regression set — and go again. Stop when a round
701
+ produces no generalisable pattern. That is the signal that the remaining failures
702
+ are the model's, not the prose's.
703
+
704
+ ### Leaving stage 1
705
+
706
+ Stage 1 is over when **both** of these hold on the same cold round, not one of
707
+ them:
708
+
709
+ 1. **Every sample holds.** Rubric compliance, and the other seven criteria, with
710
+ no regression against the round before.
711
+ 2. **What the runs wrote to memory is worth keeping.** Read `<id>/memory/` across
712
+ the whole batch, not per item. Every commit should be an operation, a pointer,
713
+ a shape or a rule — something that will still be true next week — and none
714
+ should be a live reading dressed up as a fact. A run that learned something
715
+ durable and committed nothing fails this as squarely as one that cached the
716
+ inbox.
717
+
718
+ The second criterion is the one that gets skipped, and skipping it makes stage 2
719
+ unreadable: the merged graph is built from exactly these commits, so a loose
720
+ commit policy shows up there as forty near-duplicates or as a stale fact that
721
+ makes a warm sample _worse_. Fix commit policy here, in
722
+ `agents/memory-policy-instructions.md`, while the runs are still cold and the
723
+ cause is still attributable.
724
+
725
+ When both hold, freeze the prompts, the skills and the house rules. Stage 2
726
+ measures recall against a fixed prose baseline; editing that baseline mid-stage
727
+ means a changed number has two causes.
728
+
729
+ ## Phase 8 — Stage 2: tune memory
730
+
731
+ A separate stage, with its own rounds, its own findings files and its own exit
732
+ criterion. It starts only once stage 1 is green, and what it optimises is not
733
+ correctness — stage 1 settled that — but **cost and speed through reuse**.
734
+
735
+ The premise is that a cold run rediscovers, every single time, things that do not
736
+ change: where an endpoint is, what shape it returns, which filter was the useful
737
+ one, how a repository is laid out, which agent owns what. Paid once and recalled
738
+ thereafter, that work disappears from the trajectory. A warm batch should
739
+ therefore be _dramatically_ cheaper than the cold batch it is compared against —
740
+ fewer llm calls, fewer tool calls, less wall clock — while every verdict stays
741
+ exactly where it was. Anything less than that is memory being carried and not
742
+ used, and it is a finding.
743
+
744
+ Starting early is the classic mistake. Memory that compensates for a missing
745
+ instruction is a bug that looks like a feature: the warm run passes, the prose
746
+ defect is invisible, and the failure comes back the day the graph is rebuilt.
747
+
748
+ Note the division of labour, because it is the part most often confused:
749
+
750
+ - **Stage 1 grades what an agent writes into nothing.** Each item has its own
751
+ empty graph, so criterion 3 is about whether the commit was worth making at
752
+ all.
753
+ - **Stage 2 grades what an agent reads, and what it writes on top of a graph
754
+ that already knew.** Every item starts from an identical copy of the merged
755
+ graph, so the input is the same for all of them and the comparison is clean —
756
+ and because the copies are per item, nothing one item commits can reach
757
+ another.
758
+
759
+ That second half is a question stage 1 cannot ask. Cold, every graph is empty,
760
+ so there is nothing to duplicate and nothing to supersede. Warm, an item that
761
+ commits a near-copy of a node it just recalled is growing the graph for no
762
+ information, and an item that learns the old fact was wrong and does not
763
+ supersede it leaves the next run reading the wrong thing. Both are recall-policy
764
+ findings and both are visible only here.
765
+
766
+ The only artefact stage 2 edits is `agents/memory-policy-instructions.md` — rules
767
+ about _when_ to recall, when to trust a recalled fact, when to go and look
768
+ anyway, and what to commit when memory already holds a version of it. If a
769
+ stage-2 finding cannot be written as a recall or re-commit rule, it is a stage-1
770
+ finding that escaped: note it, finish the stage, and re-open stage 1 rather than
771
+ quietly editing a prompt here.
772
+
773
+ ### The round
774
+
775
+ Merge the last good cold round's memory into one graph, then run the same cases
776
+ against a copy of it:
777
+
778
+ ```sh
779
+ zen memory merge .finetune/rounds/r2/batch/*/memory --dir .finetune/rounds/r3/memory --yes
780
+ zen memory stats --dir .finetune/rounds/r3/memory
781
+
782
+ mkdir -p .finetune/rounds/r3
783
+ zen run batch --input .finetune/rounds/r1/cases.json \
784
+ --batch-dir .finetune/rounds/r3/batch \
785
+ --memory .finetune/rounds/r3/memory --concurrency 8
786
+ ```
787
+
788
+ Same cases file, same seed, same machine as the cold round it is compared to,
789
+ and — crucially — the same flags. The cold round's `--memory .finetune/empty`
790
+ and this round's `--memory .finetune/rounds/r3/memory` differ in one thing only:
791
+ whether the directory being copied holds a graph. Everything else the agent sees
792
+ is identical, which is what makes the two rounds subtractable.
793
+
794
+ Five things follow from copying rather than sharing:
795
+
796
+ - **The source is never written.** Each item copies it, writes into the copy,
797
+ and leaves the copy behind at `<id>/memory/`. The merged graph is the same
798
+ bytes at the end of the stage as at the start.
799
+ - **`<id>/memory/` existing means nothing warm.** Cold, a directory is there
800
+ only if the item committed — a memory with no manifest is deleted after the
801
+ run. Warm, every copy arrives with the merged graph's manifest, so every item
802
+ has one whether it wrote or not. What an item actually added is in its
803
+ trajectory, as the `memory_op` nodes; read those, not the directory listing.
804
+ - **Never merge a warm round's copies back into the source.** The glob that was
805
+ right for a cold round — `r2/batch/*/memory` — matches every item of a warm
806
+ one and would fold the whole merged graph back in N times. Merge once, at the
807
+ top of the stage, from the last good _cold_ round, and never again.
808
+ - **Copies cost disk and a pause.** They are made serially, before the first
809
+ model call, one per item, and they are kept: check `zen memory stats` and
810
+ multiply by the item count before starting a round on a large graph.
811
+ - **The source must not be locked.** `zen run batch` refuses a memory another
812
+ process holds, so close any `zen memory` session first.
813
+
814
+ The merge glob above is safe because it reads the _cold_ round: an item that
815
+ committed nothing has its memory directory deleted, so `*/memory` there only
816
+ ever matches real graphs.
817
+
818
+ Read `zen memory stats` before the run. A merged graph with forty near-duplicate
819
+ nodes says the commit policy is too loose, and that is a stage-1 fix before it is
820
+ a stage-2 measurement.
821
+
822
+ Grade with the same tools and write `findings.md` the same way, comparing against
823
+ the cold round the memory came from. The heading carries `WARM`:
824
+
825
+ ```sh
826
+ .github/skills/zen-finetune/scripts/collect.sh \
827
+ -d .finetune/rounds/r3/batch -p .finetune/rounds/r2/batch compare
828
+ ```
829
+
830
+ ### What "memory used correctly" looks like
831
+
832
+ | Good | Bad |
833
+ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
834
+ | Far fewer nodes and tokens than the cold run, same verdict | Same trajectory — memory was never consulted |
835
+ | Recall replaces a discovery step: it knows the endpoint without probing | Recall replaces a _reading_: it reports yesterday's inbox as today's |
836
+ | One recall near the top, then straight to work | Repeated searches of memory mid-run, finding nothing |
837
+ | Recalled facts are checked when they are cheap to check | A recalled fact contradicts a fresh reading and the recall wins |
838
+ | Nothing is committed that memory already held | A near-copy of a node it recalled this run, committed again |
839
+ | A fact found to be wrong is superseded, not merely added to | The correction sits beside the stale node with no SUPERSEDES |
840
+ | The answer is at least as good | The answer got worse — a stale fact short-circuited the work |
841
+
842
+ The whole point is the second column of row one. Memory should shorten the
843
+ trajectory. If the warm batch costs more tokens than the cold one, memory is
844
+ being loaded and not used, and the fix is an instruction about _when_ to recall,
845
+ not more memory.
846
+
847
+ And a large saving is only good news once it has been explained. Open at least
848
+ one cheap sample's trajectory and confirm the calls that must hit the live system
849
+ still did: recall replacing discovery is the win, recall replacing a live reading
850
+ is a correctness regression wearing a speed-up's numbers.
851
+
852
+ Compare warm against cold per sample:
853
+
854
+ ```sh
855
+ for id in $(jq -r '.batch_results[].id' .finetune/rounds/r2/batch/batch.json); do
856
+ cold=$(jq -r '.usage.inputTokens + .usage.outputTokens' ".finetune/rounds/r2/batch/$id/output.json")
857
+ warm=$(jq -r '.usage.inputTokens + .usage.outputTokens' ".finetune/rounds/r3/batch/$id/output.json")
858
+ printf '%-32s cold %8s warm %8s\n' "$id" "$cold" "$warm"
859
+ done
860
+ ```
861
+
862
+ ### The stage-2 loop, and leaving it
863
+
864
+ Any sample that got longer, or got worse, is a finding, and so is any item that
865
+ re-committed what it had just recalled. Generalise it exactly as in phase 5 — a
866
+ pattern across samples, not one sample — write the rule into
867
+ `agents/memory-policy-instructions.md`, and re-run from **the same merged graph**
868
+ with the same cases into a new round. Re-merging between rounds, or merging a
869
+ warm round's own copies back into the source, changes the memory and the prose at
870
+ once; merge once per stage-2 session and keep that graph byte-identical until the
871
+ stage ends.
872
+
873
+ Stage 2 is over when, against the cold baseline:
874
+
875
+ 1. no verdict regressed;
876
+ 2. the batch is materially cheaper — a saving that is obvious next to the noise
877
+ floor, not inside it;
878
+ 3. every large saving has been traced to recall replacing discovery, not recall
879
+ replacing a live reading;
880
+ 4. what the warm items committed is either nothing or a genuine addition — no
881
+ duplicates of what they recalled, and every correction linked with
882
+ `SUPERSEDES` rather than left beside the node it contradicts.
883
+
884
+ If the project will ship with a warm memory, the merged graph from the last good
885
+ round is the one to promote into `memory/`. Read **zen-memory-warmup** first —
886
+ seeding a project's memory deliberately is that skill's whole subject, and it has
887
+ the rules about what belongs in a shipped graph.
888
+
889
+ ## The state directory
890
+
891
+ ```
892
+ .finetune/
893
+ dataset.json built once, cached, committed
894
+ rounds/
895
+ r1/ stage 1, cold
896
+ cases.json the sample; re-used verbatim by every later round, both stages
897
+ batch/ zen run batch output
898
+ findings.md the verdict block, then per-sample grading with node citations
899
+ changes.md what was edited and which findings motivated it
900
+ r2/ … stage 1, cold — until the stage-1 exit criteria hold
901
+ r3/ stage 2, warm
902
+ memory/ the merged graph; built once, copied per item, never written to
903
+ batch/ findings.md changes.md
904
+ r4/ … stage 2, warm — same merged graph, recall policy edited
905
+ ```
906
+
907
+ Commit `dataset.json`, `cases.json`, `findings.md` and `changes.md` — they are the
908
+ project's eval history and the reason a later reader can tell why an instruction
909
+ exists. The `batch/` directories are large, contain whole workspaces, and may
910
+ contain live API responses: ignore them. A warm round's is larger still — it
911
+ holds one copy of the merged graph per item. Add to `.gitignore`:
912
+
913
+ ```
914
+ .finetune/rounds/*/batch/
915
+ .finetune/empty/
916
+ ```
917
+
918
+ ## Scripts this skill ships
919
+
920
+ | Script | What it does |
921
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
922
+ | `scripts/sample.sh` | `dataset.json` → `cases.json`, stratified, rubric-first, deterministic |
923
+ | `scripts/collect.sh` | A batch directory → an index, the concatenated graphs, the run paths, the per-sample comparison against the previous round, or whether the round is gradeable at all |
924
+
925
+ Before the batch, `zen-sandbox-capacity` ships a third:
926
+ `.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh`, which
927
+ answers whether this machine can run the round you are about to start.
928
+
929
+ All find the project root from their own location and can be run from anywhere.
930
+ All are copies: `zen init` and `zen open` rewrite the whole `.github/` tree, so
931
+ an edit made here is gone at the next one. Change them upstream, in the CLI's
932
+ `templates/editor/.github/skills/` — that is the only place a change survives.
933
+
934
+ ## Rules that are easy to get wrong
935
+
936
+ 1. **Never tune against a warm memory in stage 1.** A cold run is the only one
937
+ whose result is attributable to the prose.
938
+ 2. **Never start stage 2 while a sample still fails cold.** Memory will paper
939
+ over it, the warm round will pass, and the defect returns the day the graph
940
+ is rebuilt.
941
+ 3. **Never edit a prompt, a skill or a house rule during stage 2.** The prose is
942
+ the fixed baseline recall is measured against; stage 2 edits
943
+ `agents/memory-policy-instructions.md` and nothing else.
944
+ 4. **Never tune with `--memory-read-only`.** It clamps `access` to `read`, which
945
+ takes `memory_commit` out of the schema and changes the memory prose — so a
946
+ warm round run that way differs from its cold baseline in two ways, not one.
947
+ Give `--memory` a real graph and let each item copy it.
948
+ 5. **Never merge a warm round's copies back into the stage's graph.** The merged
949
+ graph is built once, from the last good cold round, and stays byte-identical
950
+ until the stage ends.
951
+ 6. **Never change the sample and the instructions in the same round.** Two
952
+ variables, one number, no conclusion.
953
+ 7. **Never write a rule from one failing sample** unless the failure was
954
+ catastrophic. Note it and wait for the pattern.
955
+ 8. **Never edit `zen`'s four instruction files.** `zen check --fix` overwrites
956
+ them. Project policy goes in a `-policy-` file beside them.
957
+ 9. **Never adjust a rubric after seeing the run.** Fix a wrong rubric before the
958
+ round and say so, or leave it.
959
+ 10. **Never cite a finding without a node id.** If you cannot point at it in the
960
+ graph, it did not happen.
961
+ 11. **Never let a rule name the dataset.** Endpoints and entity names from the
962
+ training set inside an instruction means the eval passes and the product does
963
+ not.
964
+ 12. **Re-run `zen check` after every edit round.** A load error fails every item
965
+ identically and looks exactly like a catastrophic regression.
966
+ 13. **Never grade a round that was OOM-killed.** `collect.sh oom` before
967
+ anything else. An exit 137 is the machine, not the prose, and the items
968
+ still report `ok`.
969
+ 14. **Never compare two rounds measured on different machines**, or a warm round
970
+ against anything but the cold round its memory was merged from. Tokens and
971
+ wall clock are only comparable within one spec and one memory mode.
972
+
973
+ ## When not to fine-tune
974
+
975
+ - **The project does not load.** `zen check` first; a round on a broken project
976
+ measures nothing. Load **zen-review**.
977
+ - **The last round was OOM-killed.** Nothing about the prose is in question yet.
978
+ Load **zen-sandbox-capacity**, size the machine, re-run the same round.
979
+ - **There is no training set.** Fewer than about eight queries is a debugging
980
+ session, not a tuning loop — run them singly with `zen run` and read the graphs.
981
+ - **One run is behaving strangely.** That is diagnosis. Load **zen-inspect** and
982
+ read that run.
983
+ - **The specification changed.** Reconciling prose with intent is
984
+ **zen-spec-sync**'s job; tune after it, against what the project is meant to be.
985
+ - **The failure is the model's.** If three rounds of clear, mechanical
986
+ instructions do not move a sample, the ceiling is the model. Change the model
987
+ in `agents.yaml` and re-run the same batch — that is a legitimate experiment
988
+ and the dataset is already set up for it.
989
+
990
+ ## Related
991
+
992
+ - **zen-cli** — command surface, flags, exit codes, and what `--json` emits
993
+ - **zen-inspect** — the graph/node/ask loop, node kinds, and the symptom table
994
+ - **zen-instructions** — house rules, `requires:`, filename order, `zen check`
995
+ - **zen-memory** — the graph model, commit rules, audiences, `merge` and `stats`
996
+ - **zen-memory-warmup** — building a memory deliberately, and shipping one
997
+ - **zen-review** — the mechanical checks to run before and after every round
998
+ - **zen-sandbox-capacity** — whether the machine can run the batch; read it
999
+ before the first cold round, and whenever one dies at 137 or 124
1000
+ - **zen-spec-sync** — reconciling the project with its specification, before tuning