@zenera/cli 1.1.24 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +53 -0
  2. package/dist/ask.js +28 -5
  3. package/dist/banner.js +2 -2
  4. package/dist/batch.d.ts +6 -0
  5. package/dist/batch.js +76 -1
  6. package/dist/catalog.d.ts +11 -1
  7. package/dist/catalog.js +80 -7
  8. package/dist/commands/inspect.d.ts +47 -1
  9. package/dist/commands/inspect.js +167 -18
  10. package/dist/commands/memory.js +128 -29
  11. package/dist/commands/meta.js +311 -21
  12. package/dist/commands/models.js +96 -12
  13. package/dist/commands/open.js +4 -4
  14. package/dist/commands/run.js +38 -4
  15. package/dist/commands/sandbox.js +76 -14
  16. package/dist/engine.d.ts +5 -0
  17. package/dist/engine.js +1 -0
  18. package/dist/eventlog.d.ts +59 -0
  19. package/dist/eventlog.js +146 -0
  20. package/dist/liveness.d.ts +7 -0
  21. package/dist/liveness.js +15 -1
  22. package/dist/meta/editor.d.ts +9 -0
  23. package/dist/meta/editor.js +60 -0
  24. package/dist/meta/finetune/children.d.ts +8 -0
  25. package/dist/meta/finetune/children.js +76 -0
  26. package/dist/meta/finetune/command.d.ts +30 -0
  27. package/dist/meta/finetune/command.js +338 -0
  28. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  29. package/dist/meta/finetune/dataset/anchors.js +239 -0
  30. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  31. package/dist/meta/finetune/dataset/changes.js +319 -0
  32. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  33. package/dist/meta/finetune/dataset/command.js +1011 -0
  34. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  35. package/dist/meta/finetune/dataset/drift.js +71 -0
  36. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  37. package/dist/meta/finetune/dataset/export.js +51 -0
  38. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  39. package/dist/meta/finetune/dataset/index.js +2 -0
  40. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  41. package/dist/meta/finetune/dataset/sample.js +197 -0
  42. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  43. package/dist/meta/finetune/dataset/schema.js +216 -0
  44. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  45. package/dist/meta/finetune/dataset/store.js +177 -0
  46. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  47. package/dist/meta/finetune/dataset/types.js +39 -0
  48. package/dist/meta/finetune/failure.d.ts +15 -0
  49. package/dist/meta/finetune/failure.js +26 -0
  50. package/dist/meta/finetune/feedback.d.ts +45 -0
  51. package/dist/meta/finetune/feedback.js +85 -0
  52. package/dist/meta/finetune/improvements.d.ts +51 -0
  53. package/dist/meta/finetune/improvements.js +244 -0
  54. package/dist/meta/finetune/index.d.ts +2 -0
  55. package/dist/meta/finetune/index.js +2 -0
  56. package/dist/meta/finetune/journal.d.ts +35 -0
  57. package/dist/meta/finetune/journal.js +135 -0
  58. package/dist/meta/finetune/live.d.ts +74 -0
  59. package/dist/meta/finetune/live.js +226 -0
  60. package/dist/meta/finetune/loop.d.ts +3 -0
  61. package/dist/meta/finetune/loop.js +114 -0
  62. package/dist/meta/finetune/memories.d.ts +47 -0
  63. package/dist/meta/finetune/memories.js +154 -0
  64. package/dist/meta/finetune/report.d.ts +33 -0
  65. package/dist/meta/finetune/report.js +1284 -0
  66. package/dist/meta/finetune/retry.d.ts +24 -0
  67. package/dist/meta/finetune/retry.js +96 -0
  68. package/dist/meta/finetune/sampler.d.ts +18 -0
  69. package/dist/meta/finetune/sampler.js +41 -0
  70. package/dist/meta/finetune/system.d.ts +27 -0
  71. package/dist/meta/finetune/system.js +132 -0
  72. package/dist/meta/finetune/train.d.ts +39 -0
  73. package/dist/meta/finetune/train.js +391 -0
  74. package/dist/meta/finetune/tui.d.ts +16 -0
  75. package/dist/meta/finetune/tui.js +291 -0
  76. package/dist/meta/finetune/tuning.d.ts +167 -0
  77. package/dist/meta/finetune/tuning.js +289 -0
  78. package/dist/meta/finetune/usage.d.ts +62 -0
  79. package/dist/meta/finetune/usage.js +225 -0
  80. package/dist/meta/host.d.ts +14 -0
  81. package/dist/meta/host.js +20 -0
  82. package/dist/meta/index.d.ts +11 -0
  83. package/dist/meta/index.js +11 -0
  84. package/dist/meta/launch.d.ts +81 -0
  85. package/dist/meta/launch.js +286 -0
  86. package/dist/meta/model.d.ts +44 -0
  87. package/dist/meta/model.js +119 -0
  88. package/dist/meta/prompts.d.ts +18 -0
  89. package/dist/meta/prompts.js +61 -0
  90. package/dist/meta/relay.d.ts +20 -0
  91. package/dist/meta/relay.js +123 -0
  92. package/dist/meta/tokens.d.ts +52 -0
  93. package/dist/meta/tokens.js +226 -0
  94. package/dist/meta/window.d.ts +61 -0
  95. package/dist/meta/window.js +198 -0
  96. package/dist/meta/wire.d.ts +42 -0
  97. package/dist/meta/wire.js +129 -0
  98. package/dist/modelref.d.ts +16 -0
  99. package/dist/modelref.js +41 -0
  100. package/dist/narrate.js +5 -2
  101. package/dist/podman.d.ts +26 -0
  102. package/dist/podman.js +83 -3
  103. package/dist/projects.d.ts +0 -5
  104. package/dist/projects.js +2 -15
  105. package/dist/sandbox.d.ts +2 -0
  106. package/dist/sandbox.js +10 -3
  107. package/dist/scaffold.d.ts +15 -6
  108. package/dist/scaffold.js +5 -60
  109. package/dist/session.js +4 -29
  110. package/dist/term.d.ts +2 -0
  111. package/dist/term.js +11 -2
  112. package/dist/trace.js +53 -2
  113. package/dist/tui/models.d.ts +33 -0
  114. package/dist/tui/models.js +389 -0
  115. package/dist/tui/wrap.d.ts +5 -0
  116. package/dist/tui/wrap.js +12 -0
  117. package/dist/usage.d.ts +78 -0
  118. package/dist/usage.js +51 -0
  119. package/package.json +3 -2
  120. package/templates/editor/.github/copilot-instructions.md +31 -102
  121. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  122. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  123. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  124. package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
  125. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  126. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  127. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  128. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  129. package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
  130. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  131. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  132. package/templates/editor/.github/skills/zen-cli/references/inspect.md +47 -9
  133. package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
  134. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  135. package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
  136. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  137. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  138. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  139. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  140. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  141. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1449 -658
  142. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  143. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  144. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  145. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  146. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  147. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  148. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  149. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  150. package/templates/editor/.github/skills/zen-inspect/SKILL.md +60 -25
  151. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  152. package/templates/editor/.github/skills/zen-memory/SKILL.md +18 -14
  153. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  154. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  155. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  156. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +7 -3
  157. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
  158. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  159. package/templates/editor/.vscode/settings.json +1 -0
  160. package/templates/project/agents/files-instructions.md +29 -20
  161. package/templates/project/agents/memory-instructions.md +31 -32
  162. package/templates/project/agents/tools-instructions.md +7 -4
  163. package/templates/project/gitignore +2 -0
  164. package/dist/meta.d.ts +0 -221
  165. package/dist/meta.js +0 -684
  166. package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
  167. package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
@@ -1,6 +1,6 @@
1
1
  ---
2
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.
3
+ description: Fine-tune an agent project against a training set, a batch at a time, and every batch twice over — first with NO memory, then WITH the memory it just built. Read the whole training set, in any format, into a classified, complexity-rated dataset with rubrics; choose up to a limit of cases from it evenly by class, rubric and complex cases first. Batch 1 is a smoke test of 3 cases; each batch that goes smoothly doubles the next, up to a maximum, and each is built when it is needed from new cases plus rechecks of earlier ones — difficult ones first, then complex ones. Each batch runs with an empty memory via `zen run batch`; every trajectory is graded with `zen inspect graph`, `node` and `ask` for rubric compliance, llm-call count, fork use, assumptions, what it committed to memory and delegation; the proposals of every case are merged into edits of prompts, skills, house rules and `agents/<topic>-policy-instructions.md` files, and the same cases run again until they pass. Then what the passing run's cases committed is merged with the last good memory into a candidate, every case runs again from its own copy of it, recall and re-commits are graded the same way, the memory policy is tuned until recall makes the work dramatically cheaper without changing a verdict or re-committing what memory already held — and only then does the candidate become the last good memory and the next batch start. Cases that cause the most trouble go on a difficult list and keep coming back until they are fixed or declared stuck. The agent writes `finetune/README.md` — the plan, a mermaid map of every step, and the progress — before the first run and patches it after every step; `scripts/next.mjs` reads the state off disk so an interrupted session can resume. 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
4
  ---
5
5
 
6
6
  # Fine-tuning an agent project
@@ -10,150 +10,606 @@ made of — the agent prompts, the skills, the house rules in `agents/` — and
10
10
  it is tuned against is a dataset of real queries and the trajectories they
11
11
  produce. The model is fixed; the instructions around it are the parameters.
12
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.
13
+ It is a loop rather than a review because an instruction's effect is not
14
+ legible from reading it. A rule that reads as obvious gets ignored; a rule nobody
15
+ expected to matter turns out to be load-bearing. The only way to know which is
16
+ which is to run the queries, read the graphs, change one thing, and run them
17
+ again — which is what everything below is about.
18
+
19
+ ## Words
20
+
21
+ One word per idea, used everywhere — in findings, in the README, in reports.
22
+ When two words mean one thing, readers assume they mean two.
23
+
24
+ | Word | Means |
25
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------- |
26
+ | **case** | one query from the training set, with its rubric if it has one. `zen run batch` calls it an _item_ |
27
+ | **dataset** | every case in the training set — `finetune/dataset.json`. Never cut down. Its size is written **T** |
28
+ | **limit** (N) | how many cases this tuning uses — equal to T unless the user named a smaller number |
29
+ | **selection** | those N cases, chosen evenly by class, rubric and complex cases first — `finetune/selection.json` |
30
+ | **batch** | the cases worked on together until they pass without memory and then with it. See [What a batch is](#what-a-batch-is) |
31
+ | **batch size** | how many new cases a batch takes: `minBatch` for batch 1, doubling after each smooth batch, up to `maxBatch` |
32
+ | **recheck cases** (R) | cases from earlier batches, run again in a later one — difficult ones first, then complex ones |
33
+ | **run** | one execution of a batch's cases — a no-memory run or a with-memory run |
34
+ | **no-memory run** | every case starts from an empty memory, and writes its own; tunes the instructions |
35
+ | **with-memory run** | every case starts from its own copy of the candidate memory; tunes the memory policy |
36
+ | **proposal** | one improvement a case's grading suggests — merged with the others before anything is edited |
37
+ | **passed** | a run where every case is right AND its review found nothing left to cut |
38
+ | **concurrency** (C) | cases executing at the same time inside one run — 16 at most, halved on OOM, never below 4 |
39
+ | **trajectory** | what one case did in one run — its graph |
40
+ | **difficult case** | a case that caused real trouble — on `finetune/difficult.json`, retested until `fixed` or `stuck` |
41
+ | **candidate memory** | the last good memory plus what the batch's passing no-memory run committed — `memory.mjs path --candidate` |
42
+ | **last good memory** | every passed batch's candidate, in turn — `memory.mjs path`, always whole on disk |
43
+
44
+ A run lives in `finetune/runs/batch<NN>-<nomem|mem>-run<N>/` —
45
+ `batch03-nomem-run2` is batch 3's second run without memory, `batch03-mem-run1`
46
+ its first run with memory. Inside it, `batch/` is `zen run batch`'s own output
47
+ for that run: the command's name, not this method's.
23
48
 
24
49
  ```mermaid
25
50
  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
51
+ A[training set, any format] --> B["dataset.json<br/>every case · classified · rated · rubrics"]
52
+ B --> C["selection.json<br/>N cases, evenly by class<br/>rubric and complex first"]
53
+ C --> P["README.md<br/>the plan and the map"]
54
+ P --> E
55
+
56
+ E["batch.mjs next — build batch n<br/>3 new for batch 1, doubling while smooth<br/>+ R recheck from earlier batches"]
57
+ D[("difficult.json<br/>wrong · regressed · flaky · memory · costly")]
58
+
59
+ subgraph NM["1. WITHOUT memory: fix the instructions"]
60
+ F["zen run batch --memory finetune/empty<br/>every case starts from an EMPTY graph<br/>and writes its own"]
61
+ G["grade every case: right? what did it cost?<br/>what did it commit? ask where it went wrong"]
62
+ H{"all right, and nothing<br/>left to cut?"}
63
+ I["merge every case's proposals<br/>edit prompts · skills · rules<br/>then the SAME cases.json again"]
64
+ F --> G --> H
65
+ H -->|"no — at most 4 runs"| I --> F
37
66
  end
38
67
 
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
68
+ CAND["memory.mjs candidate<br/>last good + what this run's cases committed"]
69
+
70
+ subgraph WM["2. WITH memory: make it cheaper"]
71
+ Y["zen run batch --memory (the candidate)<br/>one COPY per case: it recalls from the copy<br/>and commits into the copy"]
72
+ N["grade memory use: recalled? cheaper?<br/>re-committed what it knew? ask why"]
73
+ Q{"still right, much cheaper,<br/>nothing re-committed?"}
74
+ O["merge the proposals<br/>edit the memory policy<br/>then the SAME cases.json again"]
75
+ Y --> N --> Q
76
+ Q -->|"no — at most 4 runs"| O --> Y
47
77
  end
48
78
 
49
- C --> D
50
- F -- yes --> H
51
- J -- yes --> L[done: commit dataset,<br/>findings and changes]
79
+ K["memory.mjs checkpoint<br/>the candidate becomes the last good memory"]
80
+
81
+ E --> F
82
+ H -->|yes| CAND --> Y
83
+ Q -->|yes| K
84
+ G -.->|"add · fix · stuck"| D
85
+ N -.-> D
86
+ D -.->|"open ones are the FIRST<br/>rechecks of every later batch"| E
87
+ K -->|"cases unused, or difficult open"| E
88
+ K -->|"every case used, none open"| V["final check<br/>the whole selection at once, last good memory"]
52
89
  ```
53
90
 
54
91
  The dataset is built once and cached. Everything after it runs many times.
55
92
 
56
- ## The two stages
93
+ ## What a batch is
94
+
95
+ The selection is not run all at once, for two reasons. After forty trajectories
96
+ nobody reads the fortieth carefully. And after one big round of edits nobody can
97
+ say which edit helped and which hurt. A batch keeps both small enough to
98
+ handle: a few cases, read in full, fixed, and **confirmed** before anything else
99
+ starts.
100
+
101
+ ### How big a batch is
102
+
103
+ Not fixed. The first batch is **3 cases** (`minBatch`): a smoke test of the
104
+ project, the sandbox, the scripts and the grading, on cases cheap enough to read
105
+ twice. A broken project, a missing key or a wrong rubric costs three trajectories
106
+ to find, not forty.
107
+
108
+ After that, `batch.mjs next` sizes each batch from how the one before went:
109
+
110
+ | The previous batch | This batch |
111
+ | ------------------------------------------------------------------------------------------------------- | ------------------------------------ |
112
+ | went smoothly — at most 2 no-memory runs, at most 2 with-memory runs, no case put on the difficult list | **twice** its size, up to `maxBatch` |
113
+ | needed more runs, or left a difficult case open | the **same** size |
57
114
 
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.
115
+ So a project the prose already fits climbs 3 → 6 → 10 in three batches, and one
116
+ that fights every rule stays small, where each run is cheap and each edit
117
+ attributable. `-m <n>` overrides it for one batch; say why in the README.
61
118
 
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 |
119
+ ### What is in a batch
70
120
 
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
121
+ There is no plan of batches drawn up in advance. `batch.mjs next` decides each
122
+ one when it is needed, from what has happened so far:
123
+
124
+ ```mermaid
125
+ flowchart LR
126
+ SEL[("selection.json<br/>class by class<br/>rubric and complex first")]
127
+ DIF[("difficult.json<br/>open · fixed · stuck")]
128
+ USED[("cases used by<br/>earlier batches")]
129
+ NEW["new cases, as many as the size<br/>not used by any batch yet"]
130
+ RC["up to R recheck cases<br/>taken in this order until R is full"]
131
+ CJ["cases.json + plan.json<br/>batch n"]
132
+ RUNS["every run of the batch<br/>no memory and with memory<br/>a COPY of that cases.json"]
133
+
134
+ SEL -->|"the next unused, in selection order"| NEW
135
+ DIF -->|"1. open — worst reason first"| RC
136
+ DIF -->|"2. fixed — what broke once breaks first"| RC
137
+ USED -->|"3. complex cases, seeded order"| RC
138
+ USED -->|"4. the rest, seeded order"| RC
139
+ DIF -.->|"selection used up: the new<br/>are open difficult cases too"| NEW
140
+ NEW --> CJ
141
+ RC --> CJ
142
+ CJ --> RUNS
143
+ ```
144
+
145
+ A `stuck` case is on none of those paths: it is out of the draw for good.
146
+
147
+ The selection is already ordered with classes taking turns, and inside a class
148
+ cases with a rubric first and the most complex first, so the first, smallest
149
+ batches are the hard, graded cases of every class — the ones that show the most
150
+ per trajectory read — and the large late batches are the simple ones. With 40
151
+ cases, a recheck of 3, and one case turning difficult in batch 3:
152
+
153
+ ```
154
+ batch 1 3 new smoke test; nothing to recheck yet
155
+ batch 2 6 new + 3 recheck batch 1 was smooth: doubled; complex first
156
+ batch 3 10 new + 3 recheck doubled again, to maxBatch
157
+ batch 4 10 new + 3 recheck the difficult case first; size held at 10
158
+ batch 5 10 new + 3 recheck ...still first until it is fixed
159
+ batch 6 1 new + 3 recheck selection used up after this one
160
+ batch 7 open difficult cases only + 3 recheck, if any are still open
161
+ ```
162
+
163
+ `batch.mjs` with no arguments says where that stands: cases used, cases left,
164
+ the next size and why, difficult cases open, and the least number of batches
165
+ still ahead. `batch.mjs next -o <run>/cases.json` also writes `plan.json`
166
+ beside it — the size, why, which cases are new and which are rechecks — because
167
+ `cases.json` itself may hold nothing but cases.
168
+
169
+ The recheck cases are there to **fail**. A rule written for batch 3 is read by
170
+ every agent on every query, and the case it breaks is almost never in batch 3.
171
+ Without rechecks that breakage is found at the final check, five batches and a
172
+ dozen rules later, when nobody can say which rule did it. With them it is found
173
+ in the next batch, when the suspect is the handful of edits since. Complex
174
+ cases come before the rest because they touch the most rules.
175
+
176
+ A batch is built **once**, for its first no-memory run. Every later run — no
177
+ memory or with memory — gets a **copy** of that `cases.json`, never a rebuild:
178
+ the rechecks depend on the difficult list, which moves, and the copy is what
179
+ makes one run comparable to the next.
180
+
181
+ ### What happens in a batch
182
+
183
+ Every case of the batch goes through the same fifteen steps, in two halves. The
184
+ first half fixes the instructions with memory out of the picture; the second
185
+ gives back the memory the first half wrote and makes the same work cheaper.
186
+
187
+ **Without memory** — runs `batch<NN>-nomem-run<N>`:
188
+
189
+ 1. **Run** the batch with no memory: every case starts from an empty graph.
190
+ 2. **Analyze** every trajectory: is it right, what did it cost, where is it
191
+ suboptimal. At each critical point, `zen inspect ask` the model why.
192
+ 3. **Read what it committed** to memory: durable facts and pointers, or live
193
+ readings dressed up as facts?
194
+ 4. **Propose** improvements, per case, each citing nodes and its `why` line.
195
+ 5. **Merge** the proposals of every case into patterns — one rule per pattern,
196
+ never one per case.
197
+ 6. **Update** the prompts, skills, house rules and policy files — create the
198
+ ones that do not exist yet.
199
+ 7. **Run again**, no memory, the same cases.
200
+ 8. **See** whether the edits worked: everything fixed, nothing broken, cheaper.
201
+ If not, back to 2. At most 4 no-memory runs.
202
+
203
+ **With memory** — runs `batch<NN>-mem-run<N>`:
204
+
205
+ 9. **Build the candidate memory** from the run that passed step 8: the last good
206
+ memory, plus what each of its cases committed, merged — `zen memory merge`
207
+ folds what the graph already held, so the same fact from two cases is one node.
208
+ 10. **Run** the batch from the candidate. Every case gets **its own copy** of it,
209
+ recalls from the copy and commits into the copy — nothing one case writes
210
+ reaches another, or the candidate.
211
+ 11. **Analyze the memory use**, against the no-memory run that passed: did it
212
+ recall, did recall replace discovery, did it go cheaper, did it re-commit
213
+ what it had just recalled? `zen inspect ask` where it did not.
214
+ 12. **Merge** the proposals into patterns.
215
+ 13. **Update** the memory policy — `agents/memory-policy-instructions.md`, and
216
+ the memory paragraphs of a prompt or skill where a `why` line names one.
217
+ 14. **Run again** from the same candidate, the same cases. Back to 11 until it
218
+ passes. At most 4 with-memory runs.
219
+ 15. **Keep it and move on**: the candidate becomes the last good memory, the
220
+ difficult and complex cases are tracked for rechecks, and the next batch is
221
+ built.
222
+
223
+ ```mermaid
224
+ flowchart LR
225
+ N1["nomem run 1"] --> G1{"right? cheap?<br/>commits sound?"}
226
+ G1 -->|"no"| FIX["merge proposals<br/>edit prose"] --> N2["nomem run 2…4<br/>the SAME cases.json"] --> G1
227
+ G1 -->|"yes — PASSED"| CAND["candidate =<br/>last good + its commits"]
228
+ CAND --> M1["mem run 1<br/>a copy per case"] --> G2{"same verdicts?<br/>much cheaper?<br/>nothing re-committed?"}
229
+ G2 -->|"no"| MFIX["merge proposals<br/>edit memory policy"] --> M2["mem run 2…4<br/>the SAME candidate"] --> G2
230
+ G2 -->|"yes — PASSED"| CPT["candidate becomes<br/>last good memory"] --> NEXT["next batch"]
231
+ ```
232
+
233
+ What each review looks at, and what a run must show to pass:
234
+
235
+ - **Correctness first, without memory.** A case that is wrong is the only work
236
+ that run has: fix it, and grade the cost after the next run.
237
+ - **Cost next, in the same run** once every case is right: llm calls per case,
238
+ missed forks, discovery it did not need. This is where most of the tuning
239
+ happens. A batch right on run 1 has **not** passed — it has reached the cost
240
+ work.
241
+ - **Memory last, with memory.** The same verdicts, a large saving, recall
242
+ replacing discovery and never a live reading, and no commit of what memory
243
+ already held.
244
+ - **Each next run is the identical `cases.json`.** The cases did not change, the
245
+ machine did not change, the memory did not change; only the prose did. So the
246
+ difference between two runs of one kind _is_ the edit — the one controlled
247
+ experiment the whole method has.
248
+ - **Passed** is the first run of a kind where every case is right, new and
249
+ recheck, **and** its review finds nothing more worth changing. That run applies
250
+ no edits, so nothing unconfirmed is carried forward.
251
+
252
+ Each kind gets **at most 4 runs** per batch. By then either the rules are wrong
253
+ rather than under-worded, or the batch is as cheap as prose will make it. Stop,
254
+ revert what the last run did not confirm, put every case still wrong on the
255
+ difficult list, and pass it.
256
+
257
+ ### Difficult cases, and why the number of batches is not fixed
258
+
259
+ A few cases cause most of the trouble, and passing their batch does not make
260
+ them easy. So they go on a list, and the list shapes every later batch.
261
+
262
+ **What makes a case difficult** — decided mechanically, after grading every run,
263
+ and recorded with `difficult.mjs add <id> --why <reason> --run <run>`:
264
+
265
+ | Reason | When |
266
+ | ----------- | ------------------------------------------------------------------------------------------ |
267
+ | `wrong` | still wrong after a run that tried to fix it — not merely wrong on run 1 |
268
+ | `regressed` | a recheck case that failed |
269
+ | `flaky` | right in one run, wrong in a later run of the same cases, no rule aimed at it |
270
+ | `memory` | with memory: wrong, no cheaper, or re-committing what it recalled — after a run that tried |
271
+ | `costly` | over 2x the batch's median llm calls in its passing run |
272
+
273
+ **How they come back.** An open difficult case is the first recheck in every
274
+ later batch until it is fixed; with more open than R, the worst reason goes
275
+ first (`wrong`, `regressed`, `flaky`, `memory`, `costly`), then the fewest
276
+ retries. Once the selection is used up, a batch is made of open difficult cases
277
+ alone, up to the batch size, plus rechecks. A difficult case right in a
278
+ `PASSED` run is marked `difficult.mjs fix <id> --run <run>`, and stays ahead of
279
+ the other rechecks after that — what was hard once breaks again first. A case
280
+ added in a batch also keeps the next batch from growing.
281
+
282
+ **Stuck.** A case still open after `maxRetries` batches have held it since it was
283
+ added (default 3) is `stuck` — the model's ceiling, a wrong rubric, or a missing
284
+ tool. It is no longer picked, and it is reported at the end with what was tried.
285
+
286
+ **When the tuning ends.** When every case in the selection has been used and no
287
+ difficult case is open. So it takes at least limit ÷ `maxBatch` batches, plus
288
+ the small ones it starts with, plus as many as its difficult cases need — and
289
+ every difficult case ends `fixed` or `stuck`, so it always ends.
290
+
291
+ Before grading a batch that holds a difficult case, read its notes
292
+ (`difficult.mjs`) and every earlier `findings.md` that names it: the rules that
293
+ were already tried and did not work are the most useful thing known about it.
294
+
295
+ ## The memory: candidate and last good
296
+
297
+ No-memory runs still write memory — each case into its own empty graph — and
298
+ what the passing no-memory run of a batch wrote is exactly what its with-memory
299
+ runs should be able to reuse. So when a no-memory run passes, it becomes a
300
+ **candidate**:
301
+
302
+ ```sh
303
+ .github/skills/zen-finetune/scripts/memory.mjs candidate batch03-nomem-run2
304
+ ```
305
+
306
+ That copies the last good memory into a new directory,
307
+ `finetune/memory/candidate-batch03/`, and folds in what each of that run's
308
+ cases committed with `zen memory merge`. Merge drops what the graph already
309
+ holds, so the same endpoint learned by four cases is one node, and a fact an
310
+ earlier batch already knew is not added twice. The candidate is what every
311
+ with-memory run of the batch starts from: `zen run batch --memory` gives **every
312
+ case its own copy**, so a case recalls from its copy, commits into its copy, and
313
+ nothing it writes reaches another case or the candidate. Every with-memory run
314
+ of the batch starts from the same bytes.
315
+
316
+ When a with-memory run passes, the candidate is kept:
317
+
318
+ ```sh
319
+ .github/skills/zen-finetune/scripts/memory.mjs checkpoint batch03-mem-run1
320
+ ```
321
+
322
+ It renames the candidate to `finetune/memory/after-batch03/` and only then
323
+ repoints `finetune/memory/last-good.json` at it. A merge writes its files one
324
+ after another, so a merge killed half-way can leave a graph whose files
325
+ disagree — but only ever a candidate, never the graph the pointer names. The
326
+ last three graphs are kept to roll back to.
327
+
328
+ ```mermaid
329
+ flowchart TD
330
+ subgraph P1["the PASSED no-memory run — each case wrote its own graph"]
331
+ MA["case a<br/>batch/a/memory"]
332
+ MB["case b<br/>batch/b/memory"]
333
+ MC["case c — committed nothing,<br/>so there is no directory"]
334
+ end
335
+
336
+ BAD["no-memory runs that did not pass"] -->|"committed under prose<br/>that was then changed"| NIL(["dropped, never merged"])
337
+
338
+ LG0[("last good memory<br/>after batch n-1")]
339
+ MRG["memory.mjs candidate — copy it,<br/>then zen memory merge each case's graph in<br/>what it already held is folded, not added"]
340
+ CAND[("candidate<br/>batch n")]
341
+
342
+ MA --> MRG
343
+ MB --> MRG
344
+ LG0 --> MRG --> CAND
345
+
346
+ subgraph P2["the with-memory runs read it, and never write to it"]
347
+ CA["case a: its own copy"]
348
+ CB["case b: its own copy"]
349
+ WR["what a case commits stays in ITS copy:<br/>evidence for grading, never merged back"]
350
+ CA --> WR
351
+ CB --> WR
352
+ end
353
+
354
+ CAND -->|"zen run batch --memory (the candidate)"| CA
355
+ CAND --> CB
356
+ PASS{"a with-memory<br/>run PASSED"}
357
+ CAND --> PASS
358
+ PASS -->|"memory.mjs checkpoint"| LG1[("last good memory<br/>after batch n")]
359
+ LG1 -->|"tuning stops for good"| PROM["promote into the project's memory/"]
360
+ ```
361
+
362
+ `next.mjs` will not let a with-memory run start without a candidate built from
363
+ the run that passed, nor the next batch be built until the candidate is kept.
364
+ So at any moment:
365
+
366
+ ```sh
367
+ .github/skills/zen-finetune/scripts/memory.mjs # both, and what built them
368
+ .github/skills/zen-finetune/scripts/memory.mjs path # the last good memory
369
+ .github/skills/zen-finetune/scripts/memory.mjs path --candidate # the batch's candidate
370
+ ```
371
+
372
+ names a whole graph holding everything the passed batches learned. If the
373
+ tuning stops for good it is the graph to promote into `memory/`. Only passing
374
+ runs go in: an earlier run of the same batch committed under prose that was then
375
+ changed, often while getting the answer wrong. And a with-memory run's copies
376
+ are never merged back — each holds the whole graph again, and what a case
377
+ committed there is graded, not kept.
378
+
379
+ A candidate with **0 nodes** means nothing was ever committed: the with-memory
380
+ run would measure nothing. That is a commit-policy finding for the no-memory
381
+ runs, and `memory.mjs candidate` says so.
382
+
383
+ ### What may be compared to what
384
+
385
+ `report.mjs compare` will diff any two runs, so this is the rule most easily
386
+ broken:
387
+
388
+ | Comparison | Valid | Why |
389
+ | -------------------------------------------------------- | ------- | ---------------------------------------------------- |
390
+ | `batch03-nomem-run2` against `batch03-nomem-run1` | **yes** | same cases, no memory, prose moved — the measurement |
391
+ | `batch03-mem-run2` against `batch03-mem-run1` | **yes** | same cases, same candidate, memory policy moved |
392
+ | `batch03-mem-run1` against the no-memory run that passed | **yes** | same cases, same prose, memory added — what it saves |
393
+ | `batch03-nomem-run1` against `batch02-nomem-run2` | **no** | different cases _and_ different prose |
394
+
395
+ Never compare two different batches. If a number has to cover the whole tuning,
396
+ it is the per-batch table in the README, read as a trend, not an experiment.
397
+
398
+ ## Why no memory first, then memory — in every batch
399
+
400
+ The two halves optimise different things and are graded on different evidence,
401
+ so they are never run together. But they are run **batch by batch**, not as two
402
+ long stages: the memory a batch builds is tested while the cases that built it,
403
+ and the rules that shaped it, are still in front of you.
404
+
405
+ | | **No-memory runs** | **With-memory runs** |
406
+ | ------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
407
+ | What is tuned | agent prompts, skills, `agents/instructions.md`, policy files — and what to commit | the memory policy: when to recall, when to trust it, what not to commit again |
408
+ | Memory at run time | `--memory finetune/empty` — every case starts empty and **writes its own** | `--memory "$(memory.mjs path --candidate)"` — every case starts from **its own copy** |
409
+ | 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 |
410
+ | The win | correctness — the right work for the right reason — then cost | speed and cost — the same verdicts for dramatically fewer tokens and less wall clock |
411
+
412
+ **Both run the same tool surface, and that is the point.** A run given a memory
413
+ directory copies it per case and lets the case write — from an empty directory
414
+ without memory, from the candidate with it — so the agent sees the same tools
74
415
  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.
416
+ across the run instead, but it also clamps every agent's `access` to `read`,
417
+ which removes `memory_commit` from the schema and changes the memory prose. That
418
+ is two variables moving at once. Keep the flag for production-shaped runs; do
419
+ not tune with it.
420
+
421
+ **No-memory runs write memory but never read it.** That is half of what they
422
+ grade. Each case commits into its own empty graph, so the commits are a clean
423
+ record of what that run _thought was worth keeping_. Criterion 3 grades exactly
424
+ that: a run that cached today's inbox as a fact has poisoned every future run,
425
+ and a run that learned a new API's shape and committed nothing has wasted the
426
+ lesson. Fix those in the no-memory runs, because the candidate is built out of
427
+ these commits.
428
+
429
+ **With-memory runs never fix a no-memory problem.** A case that is wrong with no
430
+ memory is fixed before the batch reaches memory at all — memory would hide it,
431
+ and it would come back the day the graph is rebuilt. So a with-memory run edits
432
+ only the memory policy. A with-memory finding that needs any other prose is
433
+ noted, its cases go on the difficult list, and the next batch's no-memory runs
434
+ fix it, with those cases as rechecks.
435
+
436
+ A successful with-memory run is a large drop, not a small one. With no memory,
437
+ every run rediscovers the same endpoints, the same schema, the same layout,
438
+ every time. With memory it should recall them and go straight to work. A
439
+ with-memory run that is only a few percent cheaper has not used memory; it has
440
+ merely carried it.
441
+
442
+ ## The settings
443
+
444
+ | Setting | Is | Chosen for |
445
+ | ------------------- | -------------------------------------------- | -------------------------------------------------------------------- |
446
+ | dataset size (T) | every case in the training set | nothing — it is whatever the training set holds |
447
+ | **limit** (N) | cases this tuning uses, `N = T` | nothing — the whole dataset, unless the user gives a number |
448
+ | **minBatch** | new cases in batch 1 | a smoke test: 3 |
449
+ | **maxBatch** | the most new cases a batch grows to | how many trajectories one grading pass can actually read; 8–12 |
450
+ | **recheck** (R) | earlier cases re-run in each batch | the regression guard and the difficult-case retest; 2–4 |
451
+ | **concurrency** (C) | the most cases running at once inside a run | `min(maxBatch + recheck, 16)`; halved on an out-of-memory run, min 4 |
452
+ | **seed** | makes the recheck order repeatable | anything; just never change it mid-way |
453
+ | **maxRetries** | batches a difficult case gets before `stuck` | 3 |
454
+
455
+ **Batch size is not concurrency**, and they are confused constantly because
456
+ both answer "how many at once". Batch size is how much evidence is collected
457
+ before prose may be edited; concurrency is how many of those cases run at one
458
+ time. A batch of 12 run 4 at a time is perfectly ordinary, and a batch of 3 at a
459
+ concurrency of 13 simply runs all three at once.
460
+
461
+ **Concurrency is not sized, it is backed off.** Start at
462
+ `min(maxBatch + recheck, 16)`. When a run is out of memory — `report.mjs oom`
463
+ finds an exit 137 — halve it, `C = max(4, floor(C / 2))`, write it to
464
+ `config.json` and run the same cases again. Nothing else changes it: no
465
+ preflight, no measuring, no raising it back. Out of memory at 4 means the
466
+ machine cannot run the batch — stop and tell the user.
467
+
468
+ Write them down once, in `finetune/config.json`, so every script and every
469
+ resumed session agrees:
105
470
 
106
- ## What fine-tuning may change
471
+ ```json
472
+ {
473
+ "datasetSize": 64,
474
+ "limit": 64,
475
+ "minBatch": 3,
476
+ "maxBatch": 10,
477
+ "recheck": 3,
478
+ "concurrency": 13,
479
+ "seed": 1,
480
+ "maxRetries": 3
481
+ }
482
+ ```
107
483
 
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 |
484
+ `limit`, `minBatch`, `maxBatch`, `recheck` and `seed` decide which cases go into
485
+ which batch. Changing any of them after batch 1 has run changes every batch
486
+ built after it — so fix them first and leave them alone. `concurrency` only ever
487
+ goes down, by halving, after an out-of-memory run; it is the machine, not the
488
+ experiment.
489
+
490
+ ## The README
491
+
492
+ `finetune/README.md` is the one page a person opens to understand the tuning.
493
+ **It is written by you, not generated**, and it is written for someone who has
494
+ never seen this project: plain words, real numbers, no jargon that is not in the
495
+ [Words](#words) table.
496
+
497
+ Start from [references/readme-template.md](references/readme-template.md). It has
498
+ every section, an example of each, and a comment saying when that section is
499
+ patched.
500
+
501
+ ### Before the first run: write the plan
502
+
503
+ Once the settings are fixed and the cases selected, write the README whole:
504
+
505
+ - **What this is** — one paragraph: what is being tuned, against what, why.
506
+ - **Where we are** — one bold line: batch, run (no memory or with memory), and
507
+ what is happening.
508
+ - **The plan** — the dataset by class (in the dataset / chosen / with a rubric,
509
+ from `select.mjs`'s output); what a batch is, how it grows, and its two
510
+ halves, in short paragraphs and the small loop diagram; how difficult cases
511
+ come back; the memory; the settings, each with a one-line meaning.
512
+ - **Map** — a mermaid flowchart from the dataset to the final check: one node
513
+ per batch, each with its two halves — `no memory` and `with memory` — and the
514
+ last good memory it feeds; draw the batches the growth rule gives if every
515
+ batch is smooth (3, 6, 10, 10, …). Four classes: `done`, `now`, `ahead`,
516
+ `failed`. Before the first run, the dataset and selection are `done`, batch 1's
517
+ no-memory half is `now`, everything else `ahead`. Batches the plan did not
518
+ foresee — a batch that stayed small, the ones difficult cases add — are
519
+ drawn when they are built, and a line under the map says what is left: unused
520
+ cases, open difficult cases.
521
+ - **Batches**, **Difficult cases**, **Log**, **Results so far** — empty tables
522
+ and headings.
523
+ - **Next** — the first run, in words, then the command.
524
+
525
+ ### After every step: patch it
526
+
527
+ A step is: a run finishing, a run graded, edits applied, a candidate built, a
528
+ batch passing. After each, patch — do not rewrite — these parts:
529
+
530
+ | Part | Patch |
531
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
532
+ | **Where we are** | always |
533
+ | **Map** | always — move node ids between the `class` lines; update labels (`▶ run 2`, `✔ passed · 2 runs`, `✘ 1 unfixed`); add a node for each batch the plan did not foresee and fix the sizes drawn ahead; update the "left" line |
534
+ | **Batches** | the current batch's row: size, new and recheck ids, runs so far of each kind, status, cases right (`7/8 + 3/3`), llm calls and tokens run to run, and with memory against without |
535
+ | **Difficult cases** | whenever `difficult.mjs` changes: the list as it prints, with each case's reason and what has been tried |
536
+ | **Log** | a new entry, newest first: what ran, what was found, what changed, links to `findings.md` and `changes.md` |
537
+ | **Results so far** | when a batch passes: cases used and passing out of N, instruction files changed, memory size and saving, open problems |
538
+ | **Next** | always — what `next.mjs` says, in words, then the command |
539
+
540
+ The map must always have **exactly one** `now` node, everything before it
541
+ `done` or `failed`, everything after it `ahead`. A batch that passed with a case
542
+ the prose could not fix is `failed`, not `done`, and its label says how many.
543
+
544
+ `next.mjs` checks the README's age: if a run's `batch.json`, `findings.md` or
545
+ `changes.md`, or a memory pointer, is newer than the README, its first line says
546
+ `first: patch finetune/README.md`. Do that before anything else. A README that is
547
+ only right at the end is one nobody trusts in the middle, which is the only time
548
+ it is useful.
549
+
550
+ The Plan section changes only if a setting changes — and then the Log says so
551
+ and why.
118
552
 
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.
553
+ ## What fine-tuning may change
122
554
 
123
- ## Phase 1 — Build the dataset (shared)
555
+ | File | Run kind | Changed by this loop |
556
+ | --------------------------------------------------- | ----------- | --------------------------------------------------------------------- |
557
+ | `agents/prompts/<name>.md` | no memory | Yes — the usual place a per-agent finding lands |
558
+ | `agents/skills/<name>/SKILL.md` | no memory | Yes — when the fix is knowledge, not standing policy |
559
+ | `agents/instructions.md` | no memory | Yes — when the fix binds every agent in the project |
560
+ | `agents/<topic>-policy-instructions.md` | no memory | Yes — new files are created here, one topic per file |
561
+ | `agents/memory-policy-instructions.md` | both | Yes — what to commit without memory; recall and re-commit with it |
562
+ | the memory paragraph of a prompt or skill | with memory | Only where an ask's `why` line names it |
563
+ | `agents.yaml` | — | No — a structural finding is an open problem in the README |
564
+ | `agents/memory-instructions.md` and the other three | — | **Never.** They are `zen`'s copies; `zen check --fix` overwrites them |
565
+ | The dataset | — | Only to add cases or fix a wrong rubric — never to make a run pass |
566
+
567
+ Moving a failing case's rubric to match what the agent did is not tuning. It is
568
+ deleting the test. If a rubric was wrong, say so in the run's findings and fix it
569
+ before the next run, not after seeing the result.
570
+
571
+ ## Phase 1 — Build the dataset: every case
572
+
573
+ ### All of it
574
+
575
+ Read **the whole training set** and write **every case** into the dataset. No
576
+ limit, no sampling, no "the first twenty to get going". The dataset is the
577
+ record of everything the project is meant to handle, and it is cached and reused
578
+ by every tuning after this one; cutting it here loses cases for good. How many
579
+ of them this tuning _uses_ is the limit, and the limit is applied in phase 2,
580
+ by `select.mjs`, evenly across classes — never here.
124
581
 
125
582
  ### The input is whatever the user has
126
583
 
127
584
  A training set arrives as a section of `SPECIFICATION.md`, a markdown file of
128
585
  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
586
+ screenshots with captions. There is no format to parse, so do not write a
130
587
  parser. Read the file, understand which parts are queries, and write out the
131
588
  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:
589
+ not a script's. Large sources are read in parts; the dataset is appended to as
590
+ you go, and the count is checked against the source at the end.
135
591
 
136
592
  | In the source | In the dataset |
137
593
  | ---------------------------------------------------------------- | -------------------------------------------------------------------- |
138
- | A heading like `## Query:` and a quoted string | One sample, `input` is the string |
594
+ | A heading like `## Query:` and a quoted string | One case, `input` is the string |
139
595
  | A bullet list under `### Rubric:` | `rubric` — **verbatim**, one entry per bullet |
140
596
  | An embedded image, a `![](...)` link, an audio or PDF attachment | A media part in `input`, path relative to the dataset file |
141
597
  | A paragraph of context before the query | `notes`, not `input` — unless the agent is meant to see it |
142
598
  | 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 |
599
+ | Nothing but a query | A case with no `rubric`. Still useful, selected after graded ones |
144
600
 
145
601
  Preserve the rubric word for word. A paraphrased rubric grades the paraphrase.
146
602
 
147
603
  ### The shape
148
604
 
149
- Write `.finetune/dataset.json`:
605
+ Write `finetune/dataset.json`:
150
606
 
151
607
  ```json
152
608
  {
153
609
  "version": 1,
154
610
  "source": ["SPECIFICATION.md", "docs/examples/day-planning.md"],
155
611
  "extractedAt": "2026-03-04T10:12:00Z",
156
- "samples": [
612
+ "cases": [
157
613
  {
158
614
  "id": "planning-organize-day",
159
615
  "class": "planning",
@@ -192,15 +648,15 @@ underscore only, unique across the dataset, and worth making readable:
192
648
 
193
649
  ### Classify only what the source already tells you
194
650
 
195
- The instruction is "if obvious from the input" and it should be taken literally.
196
651
  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.
652
+ API the example is about, by task type. Do not invent a taxonomy. The limit is
653
+ spread evenly by class, so the classes decide what "a fair share" means: six
654
+ classes over sixty cases is useful, twenty classes over sixty is three cases each
655
+ and the limit spreads too thin to say anything about any of them. A case that
656
+ fits nowhere gets no `class`; it is selected as `unclassified` and still gets its
657
+ share.
202
658
 
203
- ### Annotate complexity
659
+ ### Rate complexity
204
660
 
205
661
  Three levels, and rate the _trajectory_ the query demands, not the sentence:
206
662
 
@@ -210,138 +666,214 @@ Three levels, and rate the _trajectory_ the query demands, not the sentence:
210
666
  | `medium` | Several calls that depend on each other, or one delegation, or one artefact |
211
667
  | `complex` | Fan-out, multi-agent, ambiguity to resolve, or a plan that can go wrong |
212
668
 
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.
669
+ "organize my day" is three words and complex. The selection puts the complex
670
+ cases of each class first, so a rating that is too low hides a case in the late
671
+ batches; when unsure, rate up.
216
672
 
217
673
  ### It is a cache
218
674
 
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.
675
+ Extraction is expensive and non-deterministic; re-doing it per tuning would make
676
+ two tunings incomparable. So `finetune/dataset.json` is written once and read
677
+ thereafter. Re-extract only when the source changes, and keep existing ids
678
+ stable — an id that moves breaks every earlier run's findings. Commit it. It is
679
+ the project's eval set from then on. When done, report the count per class and
680
+ confirm it matches the source.
224
681
 
225
- ## Phase 2 — Sample (shared)
682
+ ## Phase 2 — Select the cases, write the README
226
683
 
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.
684
+ ### Select
230
685
 
231
686
  ```sh
232
- .github/skills/zen-finetune/scripts/sample.sh -n 16 -o .finetune/rounds/r1/cases.json
687
+ .github/skills/zen-finetune/scripts/select.mjs -o finetune/selection.json
233
688
  ```
234
689
 
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:
690
+ | Flag | Effect |
691
+ | ---------------------- | ------------------------------------------------------------------ |
692
+ | `-n`, `--limit <n>` | How many cases. Default `config.json` `limit`, else all |
693
+ | `--class <name>` | One class only — for a targeted pass after a class fails |
694
+ | `--complexity <level>` | One level only |
695
+ | `--rubric-only` | Only cases with a rubric |
696
+ | `--seed <n>` | Default `config.json` `seed`. The same seed chooses the same cases |
697
+ | `-o <file>` | Where to write; default stdout |
698
+
699
+ It chooses from **the whole dataset**, evenly by class:
700
+
701
+ 1. Cases are grouped by class.
702
+ 2. Inside a class, cases with a rubric come first, and among those the most
703
+ complex first — complex, medium, simple — then the cases with no rubric in
704
+ the same order.
705
+ 3. Classes take turns: one case from each class, then a second from each, and so
706
+ on until the limit is spent.
707
+
708
+ So a limit of 12 over 4 classes is 3 of each, however lopsided the dataset is;
709
+ a class with fewer cases than its share gives all it has and the others carry on.
710
+ Small classes are therefore over-represented relative to their share of the
711
+ dataset. That is deliberate: a class with three cases is the one most likely to
712
+ be under-instructed. It prints chosen / available per class — put that table in
713
+ the README.
714
+
715
+ `limit` is `datasetSize` — every case is tuned against. A smaller limit is only
716
+ ever the user's choice: write one only when they give a number, never to save
717
+ cost or time on your own judgement, and then say in the README which classes
718
+ lost the most. A limit below the dataset silently drops cases the project is
719
+ meant to handle, and nothing downstream will notice.
720
+
721
+ The order `select.mjs` writes is also the order batches take new cases in, so
722
+ every batch is already a mix of classes — and the first, small batches hold the
723
+ graded, complex case of each class. A graded case says _why_ it failed; a
724
+ complex one exercises the most rules per trajectory read. That is where the
725
+ first edits should come from. The simple cases arrive last, in the large
726
+ batches, when the prose has settled and they mostly confirm it.
727
+
728
+ ### Write the README
729
+
730
+ Now, before anything runs — see [The README](#the-readme).
731
+
732
+ ## Phase 3 — Build a batch, run it without memory (step 1)
247
733
 
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.
734
+ ```sh
735
+ .github/skills/zen-finetune/scripts/batch.mjs # progress: used, left, next size, difficult open
736
+ .github/skills/zen-finetune/scripts/batch.mjs next -o finetune/runs/batch01-nomem-run1/cases.json
737
+ ```
258
738
 
259
- ## Phase 3 — Stage 1: run cold
739
+ | Flag | Effect |
740
+ | ------------ | ------------------------------------------------------------- |
741
+ | _(none)_ | Progress: cases used, left, next size and why, difficult open |
742
+ | `next` | Build the next batch; with `-o`, also `plan.json` beside it |
743
+ | `-m <size>` | New cases in this batch, overriding the growth rule — say why |
744
+ | `-r <count>` | Recheck cases; default `config.json` `recheck` |
745
+ | `-o <file>` | Where to write; default stdout |
746
+ | `--seed <n>` | Default `config.json` `seed` |
260
747
 
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:**
748
+ `next` prints the size and why, which cases are new, which are rechecks and what
749
+ is left after it — put that in the README's batch row. It is built **once**, for
750
+ the first no-memory run; every later run of the batch copies that file.
263
751
 
264
752
  ```sh
265
- .github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh --concurrency 8 \
266
- && zen run batch …
753
+ RUN=finetune/runs/batch01-nomem-run1
754
+ C=$(node -p 'require("./finetune/config.json").concurrency')
755
+ .github/skills/zen-finetune/scripts/batch.mjs next -o "$RUN/cases.json"
756
+ zen run batch \
757
+ --input "$RUN/cases.json" \
758
+ --batch-dir "$RUN/batch" \
759
+ --memory finetune/empty \
760
+ --concurrency "$C"
267
761
  ```
268
762
 
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.
763
+ Then patch the README.
272
764
 
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.
765
+ `--memory` naming a directory that does not exist is how a run gets no memory:
766
+ each case gets its own empty graph, the project's real `memory/` is not read and
767
+ not touched, and every case still writes, so what it _chose_ to remember is
768
+ evidence. Never point a tuning run at the project's live memory — a graph with
769
+ content means a run can succeed by recall instead of by instruction, which is
770
+ exactly what a no-memory run exists to rule out.
771
+
772
+ Concurrency always comes from `config.json` — read it fresh for every run.
773
+ `zen run batch`'s own default is 16 and its cap 32; the tuning never goes above 16. `stdout` is the batch directory
774
+ alone, so `BATCH="$(zen run batch …)"` is safe to script. `<batch-dir>/README.md`
775
+ is a live dashboard of the run — open it while it runs.
300
776
 
301
777
  What lands:
302
778
 
303
779
  ```
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
780
+ finetune/runs/batch01-nomem-run1/
781
+ cases.json new + recheck; built once here, copied for every later run
782
+ plan.json size, why, which are new, which are rechecks
783
+ batch/
784
+ batch.json roster, counts, timings, memory mode, tokens per model
785
+ README.md the batch's own live dashboard
786
+ <id>/output.json the envelope: run.dir, run.graph, usage, output
787
+ <id>/workspace/ anything the case wrote
788
+ <id>/memory/ what the case committed, if anything
310
789
  ```
311
790
 
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.
791
+ `batch.json` carries tokens per model under `batch.models`. Read it per model — a
792
+ run that moved work onto a cheaper agent shows up there and nowhere else.
315
793
 
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.
794
+ A case that fails is data, not an outage: its `output.json` holds
795
+ `{ "ok": false, "id", "error" }` and the rest of the run still goes on. Grade the
796
+ failures too — a crash is a finding. In batch 1 a crash is most likely the
797
+ project, the sandbox or a key, not the prose: that is what the smoke test is
798
+ for. Fix it and run the three cases again before anything is graded.
319
799
 
320
- ## Phase 4 — Stage 1: grade the trajectories
800
+ ## Phase 4 — Grade without memory (steps 2–4)
801
+
802
+ ### A run with any exit 137 is void, not graded
803
+
804
+ Before grading anything, check how the run died:
321
805
 
322
806
  ```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
807
+ .github/skills/zen-finetune/scripts/report.mjs -d "$RUN/batch" oom
325
808
  ```
326
809
 
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.
810
+ A case killed at 137 ran out of memory; a case at 124 hit a timeout, which under
811
+ memory pressure is the same fault wearing a different number. Neither produced
812
+ evidence about the prompt. **Do not grade the run, do not change one word of
813
+ instruction on its basis, and do not compare its tokens to anything**. Write
814
+ `## VOID` in its `findings.md`, halve `concurrency` in `finetune/config.json`
815
+ (never below 4), and run a copy of its `cases.json` as the next run. A void run
816
+ does not count toward the run cap. `next.mjs` prints the exact value to set. Out
817
+ of memory at concurrency 4 is the one machine failure that stops the tuning:
818
+ stop and tell the user.
819
+
820
+ A 124 with no 137 is not void by itself; if timeouts repeat in the next run,
821
+ halve the concurrency the same way.
334
822
 
335
- Then work item by item. Load **zen-inspect** for the mechanics; this is what to
336
- look for.
823
+ `failures` will not catch this. A case whose command was killed usually
824
+ recovers, answers anyway and is recorded `ok`; a run can read `16 items, 16 ok,
825
+ 0 failed` with six of them OOM-killed inside. The kill is in the graph, which is
826
+ why `oom` is a separate mode and runs first.
827
+
828
+ ### Read
829
+
830
+ ```sh
831
+ .github/skills/zen-finetune/scripts/report.mjs -d "$RUN/batch"
832
+ .github/skills/zen-finetune/scripts/report.mjs -d "$RUN/batch" answers
833
+ .github/skills/zen-finetune/scripts/report.mjs -d "$RUN/batch" graphs > /tmp/batch01-run1.mmd
834
+ ```
835
+
836
+ `report.mjs` with no mode prints one line per case — ok, agent, stop reason,
837
+ tokens, seconds, rubric lines. `answers` prints each case's final answer under
838
+ its rubric. `graphs` concatenates every case's `graph.mmd`
839
+ with its id and rubric above it: one read instead of one `zen inspect graph` per
840
+ case. `compare` is the per-case cost table the findings open with, and it is the
841
+ last thing this phase does — read the trajectories before the numbers, or the
842
+ numbers decide what you look at.
843
+
844
+ **Every question about a run has a command.** Never open `output.json`,
845
+ `batch.json`, `state.json` or a memory's `graph.json` with `node -e`,
846
+ `python -c` or `jq` — the formats are the CLI's, and a guess at one costs a
847
+ probe call to find out it was wrong.
848
+
849
+ | Question | Command |
850
+ | ------------------------------------------------------ | ---------------------------------------------------------- |
851
+ | A case's trajectory directory | `report.mjs -d "$RUN/batch" paths <id>` |
852
+ | What it answered, under its rubric | `report.mjs -d "$RUN/batch" answers <id>` |
853
+ | What it committed (against the candidate, with memory) | `report.mjs -d "$RUN/batch" commits <id>` |
854
+ | Every recall and memory tool step, top recalls | `report.mjs -d "$RUN/batch" recalls <id>` |
855
+ | One memory node in full, with its links | `zen memory show <node-id> --dir "$RUN/batch/<id>/memory"` |
856
+ | A whole memory | `zen memory ls --dir <dir> --limit 200` |
857
+ | What one memory holds that another did not | `zen memory diff <base> --dir <dir>` |
858
+ | One step of a trajectory | `zen inspect node <nN> --dir <trajectory dir>` |
859
+
860
+ Then work case by case. Load **zen-inspect** for the mechanics,
861
+ **zen-analyze-run** for how to read one trajectory's decisions,
862
+ **zen-topology** for criteria 4 and 5 below, and
863
+ **zen-inspect-ask** before the first `zen inspect ask` - which node, what to
864
+ rule out first, how to word the question. The audit checklist of
865
+ zen-analyze-run (memory used and saved, delegation, forking, tool use, cost) is
866
+ what to look for under criteria 2-8 below, and its section on `zen inspect ask`
867
+ says when to question the model at a critical point. Do not write its full
868
+ report per case - `findings.md` is the report here.
337
869
 
338
870
  ### The three depths
339
871
 
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 |
872
+ | Depth | Answers | Cost |
873
+ | --------------------------------------------------------------- | -------------------------------------------------------- | -------------- |
874
+ | The graph (`graph.mmd`, or `zen inspect graph --dir <run.dir>`) | What happened, in what order, and where it looped | Free |
875
+ | `zen inspect node n13 --dir <run.dir>` | What a step actually said, sent or got back | Free |
876
+ | `zen inspect ask <llm_call-id> --question-file <f>` | Why the model chose this over that - the sentence to fix | One model call |
345
877
 
346
878
  Stay at the top for as long as it works. The `%%` header rows on the graph carry
347
879
  `nodes`, `tokens`, `thinking`, `tools`, `branches`, `compacted` and `why` — and
@@ -349,57 +881,103 @@ the `tools` row is a loop detector: the same tool with the same arguments three
349
881
  times is a finding without opening a single node.
350
882
 
351
883
  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.
884
+ impression, and impressions are how a run produces a rule nobody needed.
885
+
886
+ `ask` is for the **critical points** - the `llm_call` where the trajectory
887
+ went wrong: research started without searching memory, recalled memory ignored,
888
+ independent lookups run in series instead of forked, a handoff too early, a
889
+ tool misused or a failing call repeated, a loaded skill not followed. The graph
890
+ and the nodes show **what** it did there; only the replay says **which
891
+ sentence** made it do it, and that sentence is what phase 5 will change.
892
+
893
+ Load **zen-inspect-ask** before the first ask, then follow it for every ask:
894
+ rule out the cheap causes first
895
+ (tool not offered, instruction file absent, skill not loaded, context
896
+ compacted), pick the `llm_call` that made the choice, and word the question for
897
+ a model that sees only its own context - no node ids, "why didn't you ...", the
898
+ fixed answer shape. Never ask every call. Across a batch, ask **once per
899
+ pattern, not once per case**: the clearest case of the pattern, at the call
900
+ that made the choice. A replay carries the whole conversation up to that call,
901
+ so one ask at the right node covers the steps before it.
358
902
 
359
903
  ```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"
904
+ DIR="$(.github/skills/zen-finetune/scripts/report.mjs \
905
+ -d finetune/runs/batch01-nomem-run1/batch paths planning-organize-day | cut -f2)"
906
+ zen inspect graph --dir "$DIR"
907
+ zen inspect node n12 n13 --dir "$DIR" --part request --full
908
+ # .tmp/ask/planning-organize-day/n13-paging.md, written with your file tool:
909
+ # Why did you call /mail/list a second time instead of paging the first result?
910
+ # (then the answer shape from zen-inspect-ask)
911
+ zen inspect ask n13 --question-file .tmp/ask/planning-organize-day/n13-paging.md --dir "$DIR"
364
912
  ```
365
913
 
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.
914
+ Treat the answer as testimony, not ground truth. It is useful because it names
915
+ which instruction the model was reading, and that is the instruction you are
916
+ about to change. Grep every quote it gives in the node's `request` before using
917
+ it; an answer whose quote is not there is discarded.
369
918
 
370
919
  ### The criteria
371
920
 
372
- Grade each sample against all of these. The first is the user's; the rest apply
921
+ Grade each case against all of these. The first is the user's; the rest apply
373
922
  whether or not a rubric was written.
374
923
 
924
+ They are listed in the order to act on them. **Correctness gates everything**: a
925
+ run where a rubric line is missed, or where a recheck case regressed, has no
926
+ optimisation work in it at all — fix the correctness and grade the cost next run.
927
+ Once every case is right, criteria 2 and 4 become the objective **in that same
928
+ run** — not in some later pass — and in that order: fewer llm calls first,
929
+ because it is the number that moves most and is read straight off the graph
930
+ header; then fan-out, because a fork turns a long trajectory into a wide one.
931
+ The [cost review](#the-cost-review) below is how that is written down.
932
+
375
933
  **1. Rubric compliance.** Every line of the rubric, checked against the graph.
376
934
  Mark each `met` / `partial` / `missed` and cite the node. A rubric line naming an
377
935
  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
936
+ claiming it was made. **Recheck cases are graded exactly as hard as new ones.**
937
+ One of them failing ends the batch's progress: a rule written since is the
938
+ suspect, and it is reverted or narrowed before anything else is written.
939
+
940
+ **2. Optimality — the short path, and the assumptions that shorten it.** Three
941
+ separable faults, all visible in the graph:
942
+
943
+ - _Repeated work._ The same read twice; a search that could have been one call
944
+ made as four; a tool loop; thinking tokens spent re-deriving something already
945
+ in context; a compaction a shorter trajectory would not have needed.
946
+ - _Discovery it did not need._ A probe for something it had already been told —
947
+ it lists a directory to find a file the skill names, queries to learn a shape
948
+ the prompt states, or reads a config to confirm a default. This is the
949
+ assumption fault, and it is nearly always fixed in a skill rather than a
950
+ prompt: the agent guessed conservatively because nothing authoritative said
951
+ otherwise.
952
+ - _Assumptions that were wrong._ The mirror image, and worse — it went straight
953
+ to an answer it had no grounds for. That is criterion 7, not this one.
954
+
955
+ Record `nodes`, `llm` and `tokens` from the header. The llm-call count is the
956
+ run-to-run number; tokens are the one that swings.
957
+
958
+ **3. Memory hygiene — what it committed (step 3).** Start from
959
+ `report.mjs -d "$RUN/batch" memory` — commits per case — then
960
+ `report.mjs -d "$RUN/batch" commits` — every node each case committed, kind,
961
+ audience and text — and the commit nodes. Every commit becomes part of the candidate
962
+ the with-memory runs start from, so this is graded as hard as the answer. The rule is
387
963
  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
964
+ becomes a pointer, a call becomes the operation. A run that stored the
389
965
  _contents_ of `/mail/list` — today's inbox, a price, a status, a queue depth —
390
966
  has cached a live reading as if it were a fact, and every later run is wrong.
391
967
  What it should have stored is that the mail list is at that endpoint, what shape
392
968
  it returns and what the useful filter was. The carve-outs are narrow: an answer
393
969
  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.
970
+ check the other direction — a run that committed nothing when it learned the
971
+ shape of a new API has wasted the lesson, and its with-memory run will have
972
+ nothing to recall.
396
973
 
397
974
  **4. Fork.** Independent work should fan out; dependent work must not. Look for:
398
975
  serial calls with no data dependency between them (missed fork); branches that
399
976
  read each other's output (a fork that should have been a sequence); a fork with
400
977
  one branch (that is delegation, and should be written as delegation); branches
401
978
  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.
979
+ shape of the graph say all of it. The signature table in **zen-topology** §7 says
980
+ where each fix goes, and a missed fan-out is priced in elapsed time, not tokens.
403
981
 
404
982
  **5. Delegation.** Did it hand off to the right agent, and did it hand off at
405
983
  all? Both failures are common: doing a specialist's job inline because the prompt
@@ -419,70 +997,86 @@ be wrong by luck next time.
419
997
  **8. Failure handling.** When a tool returned an error, did it adapt, or did it
420
998
  retry the identical call? The `tools` header row finds this in one line.
421
999
 
422
- ### Write it down
1000
+ ### Write it down: findings.md
423
1001
 
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.
1002
+ One file per run, `finetune/runs/batch01-nomem-run1/findings.md`. It opens
1003
+ with a **verdict block** — a plain answer to "are we making progress", what was
1004
+ changed to get there, and the per-case table that supports it or does not — and
1005
+ only then the per-case grading.
428
1006
 
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.
1007
+ Write the verdict block last and put it first. Someone who has not read the
1008
+ earlier runs should learn from the top of the page whether the work is moving;
1009
+ someone who has should be able to stop after it.
432
1010
 
433
- The table is not typed by hand. `compare` builds it against the previous round
434
- and prints markdown ready to paste:
1011
+ The table is not typed by hand. `compare` builds it against the previous run and
1012
+ prints markdown ready to paste:
435
1013
 
436
1014
  ```sh
437
- .github/skills/zen-finetune/scripts/collect.sh \
438
- -d .finetune/rounds/r2/batch -p .finetune/rounds/r1/batch compare
1015
+ .github/skills/zen-finetune/scripts/report.mjs \
1016
+ -d finetune/runs/batch03-nomem-run2/batch \
1017
+ -p finetune/runs/batch03-nomem-run1/batch compare
439
1018
  ```
440
1019
 
441
1020
  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.
1021
+ tool calls and forks from the graph's own `%% nodes` header row, memory reads
1022
+ and commits from its `%% tools` row, the `known` count from what the commit
1023
+ results said, time from `durationMs`. The one column it leaves as `?` is
1024
+ **verdict**: whether a case is right is the judgement grading made, and no
1025
+ script can read it off a trajectory.
1026
+
1027
+ Above the table it prints a `memory:` line for this run and, when comparing, a
1028
+ `prev memory:` line for the other. Paste both. The same numbers mean opposite
1029
+ things with and without memory, and `batch.json` records
1030
+ `--memory finetune/empty` as mode `copied` exactly like a with-memory run — so
1031
+ do not read the mode out of the JSON by eye; `report.mjs` decides it by whether
1032
+ the source is a real graph.
454
1033
 
455
1034
  ```md
456
- # r2 — COLD, --concurrency 7
1035
+ # batch03-nomem-run2 — NO MEMORY, --concurrency 7
457
1036
 
458
- ## PROGRESS — one sample fixed, none broken
1037
+ ## PROGRESS — one case fixed, none broken
459
1038
 
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)
1039
+ memory: NO MEMORY — every case started from an empty graph and wrote its own (finetune/empty)
1040
+ prev memory: NO MEMORY — every case started from an empty graph and wrote its own (finetune/empty)
462
1041
 
463
- **Changed since r1:** one paragraph in `agents/skills/rag_search/SKILL.md`
1042
+ **Changed since run 1:** one paragraph in `agents/skills/rag_search/SKILL.md`
464
1043
  (+11 lines) — a question naming two products is two lookups, and the worst
465
1044
  verdict decides.
466
1045
 
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 |
1046
+ | case | verdict | tokens | llm calls | tool calls | forks | memory reads | commits (known) | time |
1047
+ | -------------------------------- | ------- | ------------------------ | --------- | ---------- | ------- | ------------ | --------------- | ------------------ |
1048
+ | info-upgrade-pair-4100-4110 | ✘ → ✔ | 923k → 2.04M (+121%) | 23 → 49 | 24 → 47 | 1 (=) | 1 (=) | 1 → 2 | 74s → 128s (+73%) |
1049
+ | info-intel-4120-compat-420 _(r)_ | ✔ → ✔ | 676k → 1.09M (+61%) | 24 → 33 | 21 → 30 | 1 (=) | 1 (=) | 1 → 1 | 70s → 79s (+13%) |
1050
+ | info-malware-profiles-medium-412 | ✘ → ✘ | 3.40M → 1.00M (−70%) | 65 → 41 | 61 → 36 | 2 (=) | 2 → 1 (−1) | 3 → 1 | 222s → 107s (−52%) |
1051
+ | code-can-vms-talk _(r)_ | ✔ → ✔ | 1.20M → 4.80M (+302%) | 35 → 124 | 32 → 113 | 1 → 5 | 1 (=) | 0 → 0 | 93s → 463s (+398%) |
1052
+ | **total** | 14 → 15 | **19.7M → 24.3M (+23%)** | 585 → 717 | 517 → 637 | 27 → 31 | 18 → 17 | 21 → 19 | 1602s → 1922s |
1053
+
1054
+ _(r)_ marks a recheck case. Both still pass, so the rule broke nothing earlier —
1055
+ which is the first thing this table has to answer.
474
1056
 
475
1057
  **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.
1058
+ +121% on it is the second lookup the rule asks for — the cost of the fix.
478
1059
 
479
1060
  **Not the rule's doing:** `code-can-vms-talk` alone is +3.6 M of the +4.5 M, and
480
1061
  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.
1062
+ case. Without it the run moves +5%, inside the noise floor.
1063
+
1064
+ **Verdict:** keep. One case fixed for eleven lines. Fix the loop next run.
482
1065
 
483
- **Verdict:** keep. One sample fixed for eleven lines. Open the loop next round.
1066
+ machine: concurrency 8 · vm 12.0 GiB / 6 cpus · host 36 GiB
484
1067
 
485
- machine: vm 12.0 GiB / 6 cpus · container 4096 MiB · index docs 3.2 GiB · host 36 GiB
1068
+ ## Cost review
1069
+
1070
+ llm calls per case: median 33; above 1.5x: `code-can-vms-talk` 124 — loop, see below.
1071
+
1072
+ - repeated work: `code-can-vms-talk` n40–n118 re-runs `rag_search` with the same
1073
+ query 27 times after an empty result; `info-malware-profiles-medium-412` n12
1074
+ and n19 are the same lookup. Pattern (2 cases + 1 in batch 1): no rule says
1075
+ what to do on an empty result → rule for the rag_search skill.
1076
+ - missed forks: `info-upgrade-pair-4100-4110` n8–n31 looks the two products up
1077
+ one after the other; they are independent → fork rule in the researcher prompt.
1078
+ - unneeded discovery: none — checked all four; every listing (n5, n6, n4, n7) is
1079
+ of a directory the skill does not name.
486
1080
 
487
1081
  ## planning-organize-day — partial
488
1082
 
@@ -497,504 +1091,701 @@ machine: vm 12.0 GiB / 6 cpus · container 4096 MiB · index docs 3.2 GiB · hos
497
1091
  - memory: committed the body of the 09:00 stand-up invite as a fact (3) — n18
498
1092
  - fork: n9 and n7 are independent and ran in sequence (4)
499
1093
  - delegation: none needed
1094
+ - why (ask n6, fork): steered-by [system prompt: planner.md › Method]
1095
+ "Work through the request one step at a time" — verified in the request
1096
+ - proposals:
1097
+ - P1 (4) planner.md › Method: narrow "one step at a time" to dependent steps;
1098
+ independent reads go out together
1099
+ - P2 (3) memory-policy: commit where a listing lives and how it is filtered,
1100
+ never what it listed
500
1101
  ```
501
1102
 
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.
1103
+ Cite the criterion number. The next phase reads down the column. A `why` line
1104
+ is the answer of an `ask` at a critical point: the node asked, the finding it
1105
+ explains, and the `steered-by` / `should-have-applied` / `missing` citation,
1106
+ marked verified or not.
1107
+
1108
+ Each case ends with its **proposals** (step 4): what that case alone says
1109
+ should change, each with the criterion, the file and the sentence its `why` line
1110
+ named. They are not edits. Phase 5 merges them across every case of the run —
1111
+ and the earlier runs' — into patterns, and only a pattern becomes an edit.
1112
+
1113
+ The `##` heading carries the verdict in one word, so a folder of runs shows the
1114
+ shape of the work without anything being opened. The `#` title carries the run
1115
+ name and `NO MEMORY` or `WITH MEMORY`. Use exactly one of:
1116
+
1117
+ | Heading | Means |
1118
+ | ------------ | ----------------------------------------------------------------------------- |
1119
+ | `PROGRESS` | at least one case fixed, none broken |
1120
+ | `NO CHANGE` | the run was clean and moved nothing — the change did not work |
1121
+ | `REGRESSION` | a case that passed now fails, whatever else improved |
1122
+ | `MIXED` | something fixed and something broken in the same run |
1123
+ | `CORRECT` | every case right, but the review found something to cut |
1124
+ | `PASSED` | **this half of the batch has passed** — every case right, nothing left to cut |
1125
+ | `VOID` | any exit 137 — halve concurrency, re-run the same cases; not graded |
1126
+ | `VERIFIED` | the final check: nothing regressed and nothing was changed |
1127
+
1128
+ `PASSED` is the one the tooling reads: a no-memory run that says `PASSED` sends
1129
+ the batch to its candidate memory, and a with-memory run that says it sends the
1130
+ batch on. Write it only when every case in the batch is right, recheck cases
1131
+ included, **and** the run's review found nothing worth changing — the
1132
+ `## Cost review` without memory, the `## Memory review` with it — so the run
1133
+ that says `PASSED` applies no edits. `next.mjs` refuses a `PASSED` from a run
1134
+ that has a `changes.md` (its edits are unconfirmed) or no review section. A run
1135
+ that is all correct with something to cut is `CORRECT`, not `PASSED`.
1136
+
1137
+ ### The cost review
1138
+
1139
+ Every run where all cases are right has a `## Cost review` section, straight
1140
+ after the verdict block, as in the example above. It is what makes the cost work
1141
+ happen rather than be skipped, so it has a fixed shape:
1142
+
1143
+ - **llm calls per case**: the median, and every case above 1.5x it with the
1144
+ reason it cost that much, citing nodes.
1145
+ - **repeated work**: the same call with the same arguments twice, a loop, a
1146
+ search made as four calls that could have been one — the pattern, or `none`.
1147
+ - **missed forks**: independent calls or sub-tasks run one after another — the
1148
+ pattern, or `none`.
1149
+ - **unneeded discovery**: listing, probing or reading to learn something a prompt
1150
+ or skill could simply have stated — the pattern, or `none`.
1151
+
1152
+ `none` needs its evidence: which trajectories were checked and what was found in
1153
+ place of the fault. A batch averaging 40+ llm calls per case almost never has
1154
+ `none` on all three, and "every case was right" is not evidence about cost. Each
1155
+ pattern found is generalised and applied exactly like a correctness finding
1156
+ (phases 5 and 6), and the next run confirms it.
1157
+
1158
+ Five rules keep the block honest; each is a run that was wasted once:
1159
+
1160
+ - **Say which memory the run had, in the title and in the `memory:` line.** A
1161
+ with-memory run graded as a no-memory one reads as an agent that knew things it
1162
+ was never told. If `compare` warns that the two runs had different memory,
1163
+ either that _is_ the measurement (a with-memory run against the no-memory run
1164
+ that passed) and the title says so, or the wrong `-p` was given.
527
1165
  - **The verdict column decides, not the token column.** Tokens swing ±2× between
528
1166
  identical runs of this kind of project. Say what noise floor you measured and
529
1167
  treat anything under it as unchanged — an aggregate move of 20% is usually
530
1168
  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
1169
+ - **Localise an aggregate change before attributing it to the edit.** Growth
1170
+ concentrated in the cases the rule touches is the price of the rule; growth
1171
+ spread evenly across all of them is prose leaking into every agent, which is a
1172
+ regression even when every case still passes.
1173
+ - **Read the call columns, not only the tokens.** Tool calls up with tokens is
1174
+ more work; tokens up with tool calls flat is a longer prefix; llm calls at
1175
+ three times the previous run is a loop that no token arithmetic will name.
1176
+ - **Name the fixed and broken cases.** "14 → 15" is not actionable and
1177
+ `info-upgrade-pair-4100-4110` is. A run that fixes one and breaks one is
542
1178
  `MIXED`, not a draw — the broken one is the more valuable finding, because it
543
1179
  is almost always the new instruction.
544
1180
 
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.
1181
+ `Verdict:` ends in keep or revert, judged by prose added per case fixed. A run
1182
+ that added forty lines and fixed nothing is reverted, not built on.
564
1183
 
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.
1184
+ The `machine:` line is not decoration. Tokens and wall clock are only comparable
1185
+ between runs **on the same spec** — a resized VM invalidates a baseline exactly
1186
+ as a prompt edit does. Concurrency is `batch.concurrency` in the run's
1187
+ `batch.json`; `podman info` gives the VM figures.
570
1188
 
571
- ### A round with any exit 137 is void, not graded
1189
+ ### Update the difficult list
572
1190
 
573
- Before grading anything, check how the round died:
1191
+ After every graded run, before anything else is written, apply the table in
1192
+ [Difficult cases, and why the number of batches is not fixed](#difficult-cases-and-why-the-number-of-batches-is-not-fixed) to every
1193
+ case in it — mechanically, not by feel:
574
1194
 
575
1195
  ```sh
576
- .github/skills/zen-finetune/scripts/collect.sh <batch-dir> oom
1196
+ D=.github/skills/zen-finetune/scripts/difficult.mjs
1197
+ $D add planning-organize-day --why wrong --run batch03-nomem-run2 \
1198
+ --note "lists mail and calendar, never proposes a schedule; rule in planner.md did not move it"
1199
+ $D add code-can-vms-talk --why costly --run batch03-nomem-run2 --note "124 llm calls, rag_search loop"
1200
+ $D add info-intel-4120-compat-420 --why memory --run batch03-mem-run2 \
1201
+ --note "recalls the endpoint, then lists the directory anyway; recall rule did not move it"
1202
+ $D fix info-upgrade-pair-4100-4110 --run batch05-nomem-run2
1203
+ $D
577
1204
  ```
578
1205
 
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:
1206
+ The note is for the batch that will face the case again: say what goes
1207
+ wrong and what has already been tried. `add` on a case that was fixed puts it
1208
+ back on the list — it was not fixed.
1209
+
1210
+ Then patch the README.
1211
+
1212
+ ## Phase 5 — Merge the proposals (step 5)
1213
+
1214
+ **First, the recheck cases.** If one failed, this phase does not happen yet. A
1215
+ case that passed in an earlier batch and fails now was broken by a rule written
1216
+ since, and the fix is to revert or narrow that rule — not to add another on top.
1217
+ Find it in the earlier batches' `changes.md`, undo or qualify it, note the
1218
+ over-reach in this run's findings, and go straight to the next run.
1219
+
1220
+ Otherwise, collect every case's proposals into one table before writing a
1221
+ single word of instruction — the correctness findings and the cost review's
1222
+ patterns alike — because the unit of change is not a proposal — it is a _pattern
1223
+ across proposals_. Two cases proposing the same sentence in different words are
1224
+ one pattern; two cases proposing opposite edits to one sentence are a conflict,
1225
+ and the conflict is the finding. Write the merged table into `findings.md`,
1226
+ under `## Proposals, merged`: one row per pattern, the cases and nodes behind
1227
+ it, the file, and the edit. A cost pattern becomes a rule the same way a
1228
+ correctness one does: three cases listing a directory to find a file the skill
1229
+ could name is a line in that skill; three cases running independent lookups in
1230
+ sequence is a fan-out rule in the prompt.
1231
+
1232
+ **One case is an anecdote.** A rule written from a single failure fixes that
1233
+ case and adds a line every other run has to read. Three cases missing the same
1234
+ beat is a rule. Two cases and a plausible mechanism is a judgement call. One case
1235
+ is a note in `findings.md` and nothing else, unless the failure was catastrophic.
1236
+
1237
+ The three do not have to be in this batch. A batch is often too small to show
1238
+ a pattern on its own, which is why the notes matter: one case here plus two in
1239
+ earlier batches is still three. Grep the earlier runs' `findings.md` before
1240
+ deciding something is an anecdote.
1241
+
1242
+ Then, for each pattern, find the _generalisable_ form. Start from the pattern's
1243
+ `why` line: a verified `steered-by` quote is the sentence to narrow or delete,
1244
+ a `should-have-applied` quote is a rule that exists but was not reached in time,
1245
+ and `no instruction — my own default` or `missing` is a rule to add. A pattern
1246
+ with no `why` line yet gets one before it gets a rule - ask at its clearest
1247
+ case's critical point, as in phase 4. "Call `/calendar/list`
1248
+ before answering a scheduling question" is not it — that is the dataset written
1249
+ into the prompt, and it will not transfer. The generalisable form is one level
1250
+ up: "when a request depends on the current state of an external system, read
1251
+ that state before proposing anything that acts on it." Same fix, and it covers
1252
+ the queries not in the dataset.
1253
+
1254
+ Two traps, both of which have cost whole batches:
612
1255
 
613
1256
  - **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.
1257
+ except in rare cases where it is clearly warranted" is read as "do X when you
1258
+ want to", because every case is clearly warranted from inside it. Either forbid
1259
+ it or do not.
617
1260
  - **Before rewriting a rule the agent keeps ignoring, grep the whole assembled
618
1261
  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-`,
1262
+ skill are concatenated. A rule obeyed nowhere usually has a second, softer copy
1263
+ somewhere the agent reads more often. The contradiction is the bug.
1264
+
1265
+ Prefer mechanical wording. "Put X before Y", "if the path does not end in `.py`,
1266
+ stop", "copy this literal verbatim" are followed. "Be thorough", "generalise
1267
+ appropriately" are followed about half the time and are not worth their tokens.
1268
+
1269
+ ## Phase 6 — Apply (step 6)
1270
+
1271
+ Route each rule by what kind of thing it is. When the pattern's `why` line
1272
+ cites a source - a prompt heading, a skill, a tool description, a house rule -
1273
+ that file is where the change goes, whatever the table below would have
1274
+ guessed; find it the way **zen-analyze-run** finds its sources of trouble, by
1275
+ grepping the quote in the request and then in `agents.yaml` and `agents/`.
1276
+
1277
+ | The finding is… | It goes in |
1278
+ | --------------------------------------------- | ---------------------------------------------------------------------------- |
1279
+ | One agent's behaviour, one agent's job | `agents/prompts/<agent>.md` |
1280
+ | A fact, a format, an API shape, a procedure | A skill — extend one, or create one |
1281
+ | A standing rule every agent must obey | `agents/instructions.md` |
1282
+ | A standing rule about one capability | `agents/<topic>-policy-instructions.md` — new file |
1283
+ | The wrong agent had the tool, or no agent did | not changed — structural: an open problem in the README, reported at the end |
1284
+ | The dataset was wrong | `finetune/dataset.json`, before the next run |
1285
+
1286
+ New policy files are the usual output of a first batch: `memory-policy-`,
642
1287
  `fork-policy-`, `tools-policy-`, `files-policy-instructions.md`. They sit beside
643
1288
  `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.
1289
+ **zen-instructions** before writing one — in particular: `requires:` is _not_
1290
+ inherited, so a policy file must repeat the condition of the file it sits next to
1291
+ (`requires: memory` beside `memory-instructions.md`), and the files are prepended
1292
+ in filename order, so `memory-policy-` reads after `memory-instructions.md`.
649
1293
 
650
1294
  Never edit `agents/memory-instructions.md`, `agents/fork-instructions.md`,
651
1295
  `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.
1296
+ of the runtime templates; `zen check --fix` replaces them wholesale. If a finding
1297
+ genuinely belongs in one — true of every Zenera project, not just this one —
1298
+ change the master upstream in the CLI templates, and note it in the run.
657
1299
 
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_.
1300
+ Record what changed and why in the run's own `changes.md` —
1301
+ `finetune/runs/batch01-nomem-run1/changes.md` — one entry per edit, each
1302
+ naming the merged pattern and the proposals behind it. It is what makes the next
1303
+ run's difference interpretable, and it is what you read when a number goes
1304
+ _down_. `next.mjs` looks for it to tell whether a graded run is waiting to be
1305
+ confirmed.
662
1306
 
663
- Then, before re-running:
1307
+ Then:
664
1308
 
665
1309
  ```sh
666
1310
  zen check
667
1311
  .github/skills/zen-review/scripts/check-paths.sh
668
1312
  ```
669
1313
 
670
- A broken project fails every item for one reason, and a round spent discovering
671
- that is a round wasted.
1314
+ and patch the README. A broken project fails every case for one reason.
672
1315
 
673
- ## Phase 7 — Stage 1: re-run cold, and leave the stage
1316
+ ## Phase 7 — Confirm without memory, build the candidate (steps 7–9)
674
1317
 
675
- Same cases file, same seed, new round directory, still no memory.
1318
+ Run the **same batch** again as the next run — a copy of its cases file,
1319
+ never a rebuild — still with no memory:
676
1320
 
677
1321
  ```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
1322
+ RUN=finetune/runs/batch01-nomem-run2
1323
+ mkdir -p "$RUN" && cp finetune/runs/batch01-nomem-run1/cases.json "$RUN/"
1324
+ C=$(node -p 'require("./finetune/config.json").concurrency')
1325
+ zen run batch --input "$RUN/cases.json" \
1326
+ --batch-dir "$RUN/batch" --memory finetune/empty --concurrency "$C"
1327
+ .github/skills/zen-finetune/scripts/report.mjs \
1328
+ -d "$RUN/batch" -p finetune/runs/batch01-nomem-run1/batch compare
681
1329
  ```
682
1330
 
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.
1331
+ Same cases, same machine, same memory, one thing changed. Read it in this order:
1332
+
1333
+ 1. **The recheck cases.** They passed before. If one now fails, the batch's new
1334
+ rule broke it — revert or narrow it and run again. Nothing else in the table
1335
+ matters until this row is clean.
1336
+ 2. **The new cases the run set out to fix.** Did the rule do what it was written
1337
+ to do?
1338
+ 3. **The new cases it was not about.** A rule aimed at one agent is read by all
1339
+ of them; something getting worse here is the same regression arriving early.
1340
+ 4. **The cost columns**, localised, as in phase 4. A cost edit that did not cut
1341
+ llm calls or tokens in the cases it was aimed at by more than the noise floor
1342
+ is reverted — prose that does not pay is a cost on every future run. A cost
1343
+ edit that made any case wrong is reverted whatever it saved.
1344
+ 5. **The commits.** A commit-policy edit should show in what the cases wrote:
1345
+ fewer live readings, the durable shape committed where it was missing.
686
1346
 
687
- Three things to watch for:
1347
+ Three things to watch across the whole tuning:
688
1348
 
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
1349
  - **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:
1350
+ dataset's own endpoints or entities, the batch passes and nothing
1351
+ generalises. Try a query the dataset does not contain, by hand.
1352
+ - **Prose growth.** If the instruction files grew by a third and one case moved,
1353
+ the batch was net negative. Delete something.
1354
+ - **A batch that will not pass.** Each kind of run gets at most 4 per batch.
1355
+ After the 4th no-memory run, stop: revert what it did not confirm, put every
1356
+ case still wrong on the difficult list, and write `PASSED` on it. If a case is
1357
+ still wrong its map node is `failed`, not `done` — the case comes back first in
1358
+ the next batches.
1359
+
1360
+ Then grade the run exactly as in phase 4, cost review and commits included. When
1361
+ every case — new and recheck — is right and the cost review finds nothing more
1362
+ worth changing, write `PASSED` at the top of that run's `findings.md` and build
1363
+ the candidate from it, before anything else:
777
1364
 
778
1365
  ```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
1366
+ .github/skills/zen-finetune/scripts/memory.mjs candidate batch01-nomem-run2
1367
+ zen memory stats --dir "$(.github/skills/zen-finetune/scripts/memory.mjs path --candidate)"
1368
+ ```
781
1369
 
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
1370
+ Read the stats before the first with-memory run. A graph with near-duplicate
1371
+ nodes or live readings in it says the commit policy is too loose — and the
1372
+ candidate is built from the run that just passed, so that is a commit-policy
1373
+ finding the no-memory grading missed. Note it; it is the first thing the
1374
+ with-memory grading will confirm or rule out.
1375
+
1376
+ Then patch the README (the batch's no-memory half `done`, its with-memory half
1377
+ `now`) and go straight on. Do not stop to ask between halves or between
1378
+ batches. The only stops are a run out of memory at concurrency 4, or the user
1379
+ asking for a pause.
1380
+
1381
+ ## Phase 8 — With memory (steps 10–15)
1382
+
1383
+ What the with-memory runs optimise is not correctness — the no-memory runs
1384
+ settled that — but **cost and speed through reuse**. A run with no memory
1385
+ rediscovers, every single time, things that do not change: where an endpoint
1386
+ is, what shape it returns, which filter was useful, how a repository is laid
1387
+ out, which agent owns what. Learned once and recalled thereafter, that work
1388
+ disappears from the trajectory. A with-memory run should therefore be
1389
+ _dramatically_ cheaper than the no-memory run that passed the same batch —
1390
+ fewer llm calls, fewer tool calls, less wall clock — with every verdict where it
1391
+ was.
1392
+
1393
+ The division of labour, because it is the part most often confused:
1394
+
1395
+ - **No-memory runs grade what an agent writes into nothing.** Each case has its
1396
+ own empty graph, so criterion 3 asks whether the commit was worth making at all.
1397
+ - **With-memory runs grade what an agent reads, and what it writes on top of a
1398
+ graph that already knew.** Every case starts from an identical copy of the
1399
+ candidate, so the comparison is clean — and because the copies are per case,
1400
+ nothing one case commits can reach another.
1401
+
1402
+ That second half cannot be seen without memory. There every graph is empty, so
1403
+ there is nothing to duplicate and nothing to supersede. Here, a case that
1404
+ commits a near-copy of a node it just recalled is spending a call to grow the
1405
+ graph by nothing, and a case that learns an old fact was wrong and does not
1406
+ supersede it leaves the next run reading the wrong thing.
1407
+
1408
+ ### Run from the candidate (step 10)
1409
+
1410
+ ```sh
1411
+ RUN=finetune/runs/batch01-mem-run1
1412
+ mkdir -p "$RUN" && cp finetune/runs/batch01-nomem-run1/cases.json "$RUN/"
1413
+ C=$(node -p 'require("./finetune/config.json").concurrency')
1414
+ zen run batch --input "$RUN/cases.json" \
1415
+ --batch-dir "$RUN/batch" \
1416
+ --memory "$(.github/skills/zen-finetune/scripts/memory.mjs path --candidate)" \
1417
+ --concurrency "$C"
786
1418
  ```
787
1419
 
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.
1420
+ The no-memory run's `--memory finetune/empty` and this run's
1421
+ `--memory "$(memory.mjs path --candidate)"` differ in one thing only: whether
1422
+ the directory being copied holds a graph. Same cases, same seed, same machine,
1423
+ same flags.
1424
+
1425
+ The new cases of the batch are recalling what they themselves committed in the
1426
+ run that passed — the easiest recall there is, and so the first test. The
1427
+ recheck cases recall what an earlier batch committed, and are where a stale
1428
+ fact, or recall replacing a live reading, shows up first.
793
1429
 
794
1430
  Five things follow from copying rather than sharing:
795
1431
 
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.
1432
+ - **The candidate is never written.** Each case copies it, writes into the copy,
1433
+ and leaves the copy at `<id>/memory/`. It is the same bytes after every
1434
+ with-memory run as before the first, so run 2 starts where run 1 did.
1435
+ - **`<id>/memory/` existing means nothing here.** Without memory a directory is
1436
+ there only if the case committed. With it every copy arrives with the
1437
+ candidate's manifest, so every case has one. What a case actually added is
1438
+ `report.mjs commits`: it diffs each copy against the candidate the batch
1439
+ started from (`zen memory diff`), marks every commit the candidate already
1440
+ held, and lists the nodes `memory_load` read.
1441
+ - **Never fold a with-memory run's copies back in.** Each copy holds the whole
1442
+ graph again; `memory.mjs candidate` refuses a with-memory run for exactly that
1443
+ reason.
808
1444
  - **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.
1445
+ model call, one per case, and kept: check `zen memory stats` and multiply by
1446
+ the case count before running on a large graph.
811
1447
  - **The source must not be locked.** `zen run batch` refuses a memory another
812
1448
  process holds, so close any `zen memory` session first.
813
1449
 
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.
1450
+ ### Analyze the memory use (step 11)
821
1451
 
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`:
1452
+ With the same tools as phase 4, and write `findings.md` the same way — the title
1453
+ says `WITH MEMORY` — comparing against **the no-memory run that passed the same
1454
+ batch**, and from run 2 on also against the previous with-memory run:
824
1455
 
825
1456
  ```sh
826
- .github/skills/zen-finetune/scripts/collect.sh \
827
- -d .finetune/rounds/r3/batch -p .finetune/rounds/r2/batch compare
1457
+ R=finetune/runs
1458
+ .github/skills/zen-finetune/scripts/report.mjs -d $R/batch01-mem-run1/batch oom
1459
+ .github/skills/zen-finetune/scripts/report.mjs -d $R/batch01-mem-run1/batch memory
1460
+ .github/skills/zen-finetune/scripts/report.mjs -d $R/batch01-mem-run1/batch recalls
1461
+ .github/skills/zen-finetune/scripts/report.mjs -d $R/batch01-mem-run1/batch commits
1462
+ .github/skills/zen-finetune/scripts/report.mjs \
1463
+ -d $R/batch01-mem-run1/batch -p $R/batch01-nomem-run2/batch compare
828
1464
  ```
829
1465
 
830
- ### What "memory used correctly" looks like
1466
+ That is the one comparison where the two runs are _meant_ to differ in memory,
1467
+ and `compare` says so in the note under it. Grade correctness first, exactly as
1468
+ in phase 4: a case that was right without memory and is wrong with it is the
1469
+ worst finding a with-memory run can have.
831
1470
 
832
1471
  | Good | Bad |
833
1472
  | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
834
- | Far fewer nodes and tokens than the cold run, same verdict | Same trajectory — memory was never consulted |
1473
+ | Far fewer nodes and tokens than without memory, same verdict | Same trajectory — memory was never consulted |
835
1474
  | Recall replaces a discovery step: it knows the endpoint without probing | Recall replaces a _reading_: it reports yesterday's inbox as today's |
836
1475
  | One recall near the top, then straight to work | Repeated searches of memory mid-run, finding nothing |
837
1476
  | 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 |
1477
+ | Nothing is committed that memory already held — `known` is 0 | A near-copy of a node it recalled this run, committed again |
839
1478
  | A fact found to be wrong is superseded, not merely added to | The correction sits beside the stale node with no SUPERSEDES |
840
1479
  | The answer is at least as good | The answer got worse — a stale fact short-circuited the work |
841
1480
 
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.
1481
+ **Re-committing is a cost finding, not a hygiene nicety.** Every
1482
+ `memory_commit` of something the graph held is an llm turn and a tool call that
1483
+ bought nothing — `zen memory merge` would have folded it anyway. The `commits
1484
+ (known)` column and `report.mjs memory` count them per case, and
1485
+ `report.mjs commits` names them — `ALREADY HELD as <id>`, with the nearest
1486
+ candidate node and its score beside every other commit; the target with
1487
+ memory is **zero known**, and commits only of what the run genuinely learned
1488
+ that the graph did not have. A case that recalled a node and then committed the
1489
+ same fact in its own words is the pattern to ask about.
1490
+
1491
+ If a with-memory run costs more than the no-memory run, memory is being loaded
1492
+ and not used; the fix is an instruction about _when_ to recall, not more memory.
1493
+ It is `NO CHANGE` at best. And a large saving is only good news once explained:
1494
+ open at least one cheap case's trajectory and confirm the calls that must hit
1495
+ the live system still did.
1496
+
1497
+ The bad column is where to ask. A trajectory that never consulted memory,
1498
+ recalled and then redid the work, or committed what it had recalled, has a
1499
+ critical point at the first research call, the call right after the recall, or
1500
+ the call that chose to commit: ask it why, following the memory rows of
1501
+ **zen-analyze-run** §5 and the wording rules of **zen-inspect-ask**, before
1502
+ writing a word of policy.
1503
+
1504
+ Every with-memory `findings.md` has a `## Memory review` straight after the
1505
+ verdict block — `next.mjs` refuses a `PASSED` without one — with a fixed shape:
1506
+
1507
+ - **saving**: llm calls, tool calls, tokens and wall clock against the no-memory
1508
+ run that passed, per case and in total, and the noise floor it is read against.
1509
+ - **recall**: per case, whether recall replaced discovery, citing the recall node
1510
+ and the discovery it saved — or `none`, with the trajectory checked.
1511
+ - **live readings**: any recalled fact used in place of a reading that must be
1512
+ live — the pattern, or `none`.
1513
+ - **re-commits**: commits of what the graph held (`known`), and near-copies of
1514
+ recalled nodes — the pattern, or `none`.
1515
+ - **corrections**: facts found wrong and whether they were superseded — or `none`.
1516
+
1517
+ Then the per-case grading, each case ending in its proposals, as in phase 4.
1518
+
1519
+ ### Merge, update the memory policy, run again (steps 12–14)
1520
+
1521
+ Merge the proposals into patterns exactly as in phase 5, and write the merged
1522
+ table under `## Proposals, merged`. Then apply them — to
1523
+ `agents/memory-policy-instructions.md`, or to the memory paragraph of a prompt
1524
+ or skill where an ask's `why` line names one: rules about _when_ to recall, when
1525
+ to trust a recalled fact, when to go and look anyway, and when **not** to
1526
+ commit because memory already holds it. Record them in this run's `changes.md`
1527
+ and run `zen check`.
1528
+
1529
+ A pattern that cannot be written as one of those is a no-memory finding that
1530
+ escaped: the with-memory runs do not fix it. Write it under "Open problems" in
1531
+ the README, put its cases on the difficult list, and leave the prose alone — the
1532
+ next batch's no-memory runs, with those cases as rechecks, are where it is
1533
+ fixed. A pattern about what is _in_ the candidate — a live reading committed as
1534
+ a fact — is a commit-policy rule in `agents/memory-policy-instructions.md`, and
1535
+ the offending nodes may be removed from the candidate with
1536
+ `zen memory forget --dir "$(memory.mjs path --candidate)" <id>…`, recorded in
1537
+ `changes.md`; nothing else edits the candidate by hand.
1538
+
1539
+ Then run **the same batch** again from **the same candidate**:
1540
+ `batch01-mem-run2`, a copy of the batch's cases. Read it against the previous
1541
+ with-memory run (the effect of the policy edit) and against the no-memory run
1542
+ that passed (the saving). A with-memory run passes on `PASSED` exactly as a
1543
+ no-memory one does, with at most 4 runs, and the difficult list is updated after
1544
+ every graded run — a case memory makes wrong or no cheaper, still, after a run
1545
+ that tried to fix it, is `--why memory`.
1546
+
1547
+ ### Keep it, and move on (step 15)
1548
+
1549
+ A with-memory run passes when, against the no-memory run that passed:
846
1550
 
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.
1551
+ 1. no verdict regressed;
1552
+ 2. the run is materially cheaper — a saving obvious next to the noise floor;
1553
+ 3. every large saving is traced to recall replacing discovery, not a live
1554
+ reading;
1555
+ 4. what the cases committed is either nothing or a genuine addition — `known`
1556
+ is 0, no near-copies of what they recalled, every correction linked with
1557
+ `SUPERSEDES`.
851
1558
 
852
- Compare warm against cold per sample:
1559
+ Write `PASSED`, then keep the candidate before anything else:
853
1560
 
854
1561
  ```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
1562
+ .github/skills/zen-finetune/scripts/memory.mjs checkpoint batch01-mem-run1
860
1563
  ```
861
1564
 
862
- ### The stage-2 loop, and leaving it
1565
+ Then patch the README (the batch `done`, the next one `now`), mark any difficult
1566
+ case right in both passing runs `fixed`, and build the next batch as `next.mjs`
1567
+ says — straight away. It goes on while the selection has unused cases or the
1568
+ difficult list has open ones; then comes the final check.
863
1569
 
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.
1570
+ ## The final check
872
1571
 
873
- Stage 2 is over when, against the cold baseline:
1572
+ When every case in the selection has been used and no difficult case is open,
1573
+ run **the whole selection** as one batch — the last good memory, the same flags,
1574
+ the same machine — into `finetune/runs/final-check/`. It is the only run that
1575
+ measures every case under the final prose and the final memory at once: the
1576
+ first batches were tuned against prose that later batches changed, and only
1577
+ their rechecks have seen it since.
874
1578
 
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.
1579
+ ```sh
1580
+ RUN=finetune/runs/final-check
1581
+ mkdir -p "$RUN"
1582
+ cp finetune/selection.json "$RUN/cases.json"
1583
+ C=$(node -p 'require("./finetune/config.json").concurrency')
1584
+ zen run batch --input "$RUN/cases.json" --batch-dir "$RUN/batch" \
1585
+ --memory "$(.github/skills/zen-finetune/scripts/memory.mjs path)" --concurrency "$C"
1586
+ ```
1587
+
1588
+ Grade it, and head `findings.md` with `VERIFIED` if no verdict regressed and the
1589
+ saving holds. Change nothing. If something regressed, that is a real finding:
1590
+ write it under "Open problems" with the batch whose rules are the suspect, and
1591
+ report it. Then patch the README into its
1592
+ final form: every map node `done` or `failed`, "Results so far" as the summary of
1593
+ the whole tuning, and every `stuck` case with what was tried.
1594
+
1595
+ If the project will ship with a memory, `memory.mjs path` is the graph to promote
1596
+ into `memory/` — and it is that graph whenever the tuning stops, finished or not.
1597
+ Read **zen-memory-warmup** first — it has the rules about what belongs in a
1598
+ shipped graph.
888
1599
 
889
1600
  ## The state directory
890
1601
 
891
1602
  ```
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
1603
+ finetune/
1604
+ dataset.json every case in the training set; built once, cached, committed
1605
+ config.json limit, minBatch, maxBatch, recheck, concurrency, seed, maxRetries
1606
+ selection.json the cases up to the limit, evenly by class, rubric and complex first
1607
+ difficult.json the difficult cases: why, when, notes, fixed — written by difficult.mjs
1608
+ README.md the plan, the map and the progress — written by you, patched every step
1609
+ USAGE.md tokens per model, batch, run and stage — written by usage.mjs, never by hand
1610
+ usage.json the same numbers, for a script
1611
+ usage/
1612
+ ledger.jsonl one line per meta agent call, run, batch and inspect ask — appended by zen
1613
+ empty/ never created; naming it is what gives a run no memory
1614
+ memory/
1615
+ candidate.json the batch being tuned: its candidate, and the run it was built from
1616
+ candidate-batch<NN>/ last good + that run's commits; what the with-memory runs copy
1617
+ last-good.json which graph is last good, and which batches built it
1618
+ after-batch<NN>/ a kept candidate, one per passed batch; the last three are kept
1619
+ runs/
1620
+ batch01-nomem-run1/
1621
+ cases.json new + recheck; built once, copied for every later run of the batch
1622
+ plan.json the size and why, which cases are new, which are rechecks
897
1623
  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
1624
+ findings.md the verdict block, the cost review, per-case grading, proposals merged
1625
+ changes.md what was edited and which merged proposals motivated it
1626
+ batch01-nomem-run2/ same cases, new prose. PASSED here builds the candidate
1627
+ batch01-mem-run1/ same cases, from the candidate; findings.md has a memory review
1628
+ batch01-mem-run2/ same candidate, new memory policy. PASSED here keeps it
1629
+ batch02-nomem-run1/ the next batch: twice as many new cases + rechecks
1630
+ batch07-nomem-run1/ past the selection: open difficult cases only
1631
+ final-check/ the whole selection at once, with the last good memory
905
1632
  ```
906
1633
 
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`:
1634
+ The `batch<NN>-<nomem|mem>-run<N>` naming is not cosmetic: `next.mjs` parses it
1635
+ to find which batch a run belongs to, which half it is in and whether it has
1636
+ passed, and `batch.mjs` and `difficult.mjs` read which cases each batch held
1637
+ from its first no-memory run. Any other directory under `runs/` is ignored.
1638
+
1639
+ Commit `dataset.json`, `config.json`, `selection.json`, `difficult.json`,
1640
+ `README.md`, `USAGE.md`, `usage.json`, every `cases.json`, `plan.json`,
1641
+ `findings.md` and `changes.md` —
1642
+ they are the
1643
+ project's eval history. The `batch/` directories are large, contain whole
1644
+ workspaces, and may contain live API responses: ignore them, and the memory
1645
+ graphs built from them, and the raw usage ledger. Add to `.gitignore`:
912
1646
 
913
1647
  ```
914
- .finetune/rounds/*/batch/
915
- .finetune/empty/
1648
+ finetune/runs/*/batch/
1649
+ finetune/memory/
1650
+ finetune/empty/
1651
+ finetune/usage/
916
1652
  ```
917
1653
 
1654
+ ## The usage report
1655
+
1656
+ `finetune/USAGE.md` says what the tuning has cost in tokens — in total, by
1657
+ model, by batch, by run, the costliest cases, and the meta
1658
+ agent's own spend by stage and session. Three things spend tokens, and only one
1659
+ is the project under test: the **project agents** (every case of every run, read
1660
+ off `batch.json`, so runs from before the ledger count too), the **meta agent**
1661
+ doing the tuning, and the **`zen inspect ask`** calls of the grading.
1662
+
1663
+ Nobody writes it by hand: `next.mjs` regenerates it on every call, and a running
1664
+ `zen meta` does every five minutes and when it exits. `zen meta`, `zen run`,
1665
+ `zen run batch` and `zen inspect ask` append to `finetune/usage/ledger.jsonl`
1666
+ whenever the project has a `finetune/`. Link it from the README instead of
1667
+ copying its numbers; `usage.json` holds the same numbers for a script.
1668
+
918
1669
  ## Scripts this skill ships
919
1670
 
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 |
1671
+ | Script | What it does |
1672
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1673
+ | `scripts/select.mjs` | `dataset.json` → `selection.json`: up to the limit, evenly by class, rubric then complex first — the order batches take new cases in |
1674
+ | `scripts/batch.mjs` | builds the next batch's `cases.json` and `plan.json`: its size by the growth rule, new cases from the selection + rechecks, difficult then complex first; only difficult ones once the selection is used. No command: progress |
1675
+ | `scripts/report.mjs` | one run's `batch/` → an index, the concatenated graphs, the trajectory paths, the failures, the OOM check, memory use per case, each case's commits (against the candidate, with memory), recalls and answers, or the comparison against another run |
1676
+ | `scripts/difficult.mjs` | the difficult list: `add` / `fix` a case with its reason and run; with no command, the list with `open` / `fixed` / `stuck` and retry counts |
1677
+ | `scripts/memory.mjs` | `candidate <run>` builds a batch's candidate from a passed no-memory run; `checkpoint <run>` keeps it after a passed with-memory run; `path [--candidate]` names a graph; no command says what built them |
1678
+ | `scripts/next.mjs` | reads `finetune/` and prints the next step — including when a candidate or checkpoint is missing and when the final check is due — and whether the README has fallen behind; refreshes `USAGE.md` on the way |
1679
+ | `scripts/usage.mjs` | the ledger and every run's `batch.json` → `USAGE.md` and `usage.json`: tokens by who spent them, model, batch, run and stage; `--stdout` prints it instead |
924
1680
 
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.
1681
+ The README template is `references/readme-template.md`.
928
1682
 
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.
1683
+ All seven are Node programs and run on the Node that `zen` itself requires — no
1684
+ `jq`, and they work the same on Windows. `memory.mjs` also needs `zen` on PATH. They find the project root from their
1685
+ own location and can be run from anywhere. They are copies: `zen init` and
1686
+ `zen open` rewrite the whole `.github/` tree, so an edit made here is gone at the
1687
+ next one. Change them upstream, in the CLI's `templates/editor/.github/skills/`.
933
1688
 
934
1689
  ## Rules that are easy to get wrong
935
1690
 
936
- 1. **Never tune against a warm memory in stage 1.** A cold run is the only one
1691
+ 1. **Never give a no-memory run memory.** A run with no memory is the only one
937
1692
  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.
1693
+ 2. **Never run a batch with memory before it has passed without it.** Memory
1694
+ will paper over the defect, and it returns the day the graph is rebuilt.
1695
+ 3. **Never edit anything but the memory policy in a with-memory run.**
1696
+ `agents/memory-policy-instructions.md`, or the memory paragraph a `why` line
1697
+ names. Anything else goes to "Open problems" and the next batch's no-memory
1698
+ runs.
944
1699
  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
1700
+ takes `memory_commit` out of the schema and changes the memory prose — two
1701
+ differences from the no-memory baseline, not one.
1702
+ 5. **Never build a candidate from anything but a PASSED no-memory run, and never
1703
+ fold a with-memory run's copies into it.** Each copy holds the whole graph
1704
+ again. Every case of a with-memory run gets its own copy of the candidate;
1705
+ the candidate itself is never written by a run.
1706
+ 6. **Never change the cases and the instructions in the same run.** Two
1707
+ variables, one number, no conclusion. Edits go in after a run is graded, and
1708
+ the next run re-runs a **copy** of the batch's `cases.json`.
1709
+ 7. **Never compare two different batches.** Only run to run within a batch and
1710
+ kind, and a with-memory run against the no-memory run that passed the same
1711
+ batch.
1712
+ 8. **Never treat batch size as concurrency.** Batch size is chosen so every
1713
+ trajectory in the batch gets read; concurrency is
1714
+ `min(maxBatch + recheck, 16)`, halved on each out-of-memory run, never below 4.
1715
+ 9. **Never let a recheck case's failure be outranked.** A new case failing is
1716
+ work to do; a recheck case failing means a recent rule broke something that
1717
+ worked. Revert or narrow it before writing anything new.
1718
+ 10. **Never change `limit`, `minBatch`, `maxBatch`, `recheck` or `seed` once
1719
+ batch 1 has run, and never rebuild a batch for a later run — copy its
1720
+ `cases.json`.** What goes into a batch depends on all five and on the
1721
+ difficult list at the time it was built. `concurrency` only goes down, by
1722
+ halving after an OOM. A batch's size comes from `batch.mjs`, not from you —
1723
+ `-m` only with a reason in the README.
1724
+ 11. **Never write `PASSED` on a run that is only correct.** It means every case is
1725
+ right, recheck cases included, **and** the run's review — cost without
1726
+ memory, memory with it — found nothing to cut, so the run applies no edits.
1727
+ All right with something to cut is `CORRECT`.
1728
+ 12. **Never cut the dataset, and never set `limit` below `datasetSize` on your
1729
+ own.** Every case in the training set goes into `dataset.json`, and every
1730
+ case is tuned against unless the user named a smaller limit.
1731
+ 13. **Patch the README after every step, by hand.** Exactly one `now` node on the
1732
+ map. When `next.mjs` says `first: patch finetune/README.md`, do that first.
1733
+ 14. **Never edit from one case's proposal.** Merge every case's proposals first;
1734
+ a rule needs a pattern, unless the failure was catastrophic.
1735
+ 15. **Never edit `zen`'s four instruction files.** `zen check --fix` overwrites
1736
+ them. Project policy goes in a `-policy-` file beside them.
1737
+ 16. **Never adjust a rubric after seeing the run.** Fix a wrong rubric before the
1738
+ run and say so, or leave it.
1739
+ 17. **Never cite a finding without a node id.** If you cannot point at it in the
960
1740
  graph, it did not happen.
961
- 11. **Never let a rule name the dataset.** Endpoints and entity names from the
1741
+ 18. **Never let a rule name the dataset.** Endpoints and entity names from the
962
1742
  training set inside an instruction means the eval passes and the product does
963
1743
  not.
964
- 12. **Re-run `zen check` after every edit round.** A load error fails every item
1744
+ 19. **Run `zen check` after every round of edits.** A load error fails every case
965
1745
  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.
1746
+ 20. **Never grade a run that was OOM-killed.** `report.mjs oom` before anything
1747
+ else. An exit 137 is the machine, not the prose, and the cases still report
1748
+ `ok`. Halve the concurrency and run the same cases again.
1749
+ 21. **Never compare two runs measured on different machines.** Tokens and wall
1750
+ clock are only comparable within one spec.
1751
+ 22. **Never build the next batch before `memory.mjs checkpoint`.** A passed
1752
+ batch's candidate becomes the last good memory before anything else, or a
1753
+ stop at the wrong moment loses it. `next.mjs` insists.
1754
+ 23. **Never let a difficult case go unrecorded.** Wrong after a fix, a failed
1755
+ recheck, flaky, made worse or no cheaper by memory, or over 2x the median:
1756
+ `difficult.mjs add`, with a note saying what was tried. A case forgotten when
1757
+ its batch passes is the one that fails in production.
1758
+ 24. **Never retry a `stuck` case again.** It had `maxRetries` batches. Report it
1759
+ with what was tried; more prose will not move the model's ceiling.
1760
+ 25. **Never pass a with-memory run that re-commits what it recalled.** `known`
1761
+ above 0 is a call spent on nothing, in every future run. It is a finding for
1762
+ the memory policy, not noise.
972
1763
 
973
1764
  ## When not to fine-tune
974
1765
 
975
- - **The project does not load.** `zen check` first; a round on a broken project
1766
+ - **The project does not load.** `zen check` first; a run on a broken project
976
1767
  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.
1768
+ - **The last run was OOM-killed.** Nothing about the prose is in question yet.
1769
+ Halve the concurrency (never below 4) and run the same cases again; at 4, stop
1770
+ and tell the user.
979
1771
  - **There is no training set.** Fewer than about eight queries is a debugging
980
1772
  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.
1773
+ - **One run is behaving strangely.** That is diagnosis. Load **zen-analyze-run**
1774
+ and analyze that run.
983
1775
  - **The specification changed.** Reconciling prose with intent is
984
1776
  **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.
1777
+ - **The failure is the model's.** If three runs of clear, mechanical instructions
1778
+ do not move a case, the ceiling is the model. Change the model in `agents.yaml`
1779
+ and run the same batch again — a legitimate experiment, and the cases are
1780
+ already set up for it.
989
1781
 
990
1782
  ## Related
991
1783
 
992
1784
  - **zen-cli** — command surface, flags, exit codes, and what `--json` emits
993
1785
  - **zen-inspect** — the graph/node/ask loop, node kinds, and the symptom table
1786
+ - **zen-analyze-run** — auditing one trajectory, and asking the model why at its critical points
994
1787
  - **zen-instructions** — house rules, `requires:`, filename order, `zen check`
995
1788
  - **zen-memory** — the graph model, commit rules, audiences, `merge` and `stats`
996
1789
  - **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
1790
+ - **zen-review** — the mechanical checks to run before and after every batch
1000
1791
  - **zen-spec-sync** — reconciling the project with its specification, before tuning