@zenera/cli 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +88 -2
  2. package/dist/ask.d.ts +51 -0
  3. package/dist/ask.js +166 -0
  4. package/dist/banner.js +2 -2
  5. package/dist/batch.d.ts +83 -0
  6. package/dist/batch.js +413 -0
  7. package/dist/catalog.d.ts +11 -1
  8. package/dist/catalog.js +80 -7
  9. package/dist/command.d.ts +6 -0
  10. package/dist/commands/inspect.d.ts +57 -0
  11. package/dist/commands/inspect.js +539 -91
  12. package/dist/commands/list.js +128 -19
  13. package/dist/commands/memory.js +461 -32
  14. package/dist/commands/meta.js +311 -21
  15. package/dist/commands/models.js +110 -14
  16. package/dist/commands/open.js +4 -4
  17. package/dist/commands/run.js +175 -36
  18. package/dist/commands/sandbox.js +148 -15
  19. package/dist/engine.d.ts +24 -0
  20. package/dist/engine.js +61 -8
  21. package/dist/eventlog.d.ts +59 -0
  22. package/dist/eventlog.js +146 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +20 -3
  25. package/dist/main.js +5 -2
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +60 -0
  28. package/dist/meta/finetune/children.d.ts +8 -0
  29. package/dist/meta/finetune/children.js +76 -0
  30. package/dist/meta/finetune/command.d.ts +30 -0
  31. package/dist/meta/finetune/command.js +338 -0
  32. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  33. package/dist/meta/finetune/dataset/anchors.js +239 -0
  34. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  35. package/dist/meta/finetune/dataset/changes.js +319 -0
  36. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  37. package/dist/meta/finetune/dataset/command.js +1011 -0
  38. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  39. package/dist/meta/finetune/dataset/drift.js +71 -0
  40. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  41. package/dist/meta/finetune/dataset/export.js +51 -0
  42. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  43. package/dist/meta/finetune/dataset/index.js +2 -0
  44. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  45. package/dist/meta/finetune/dataset/sample.js +197 -0
  46. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/schema.js +216 -0
  48. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  49. package/dist/meta/finetune/dataset/store.js +177 -0
  50. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  51. package/dist/meta/finetune/dataset/types.js +39 -0
  52. package/dist/meta/finetune/failure.d.ts +15 -0
  53. package/dist/meta/finetune/failure.js +26 -0
  54. package/dist/meta/finetune/feedback.d.ts +45 -0
  55. package/dist/meta/finetune/feedback.js +85 -0
  56. package/dist/meta/finetune/improvements.d.ts +51 -0
  57. package/dist/meta/finetune/improvements.js +244 -0
  58. package/dist/meta/finetune/index.d.ts +2 -0
  59. package/dist/meta/finetune/index.js +2 -0
  60. package/dist/meta/finetune/journal.d.ts +35 -0
  61. package/dist/meta/finetune/journal.js +135 -0
  62. package/dist/meta/finetune/live.d.ts +74 -0
  63. package/dist/meta/finetune/live.js +226 -0
  64. package/dist/meta/finetune/loop.d.ts +3 -0
  65. package/dist/meta/finetune/loop.js +114 -0
  66. package/dist/meta/finetune/memories.d.ts +47 -0
  67. package/dist/meta/finetune/memories.js +154 -0
  68. package/dist/meta/finetune/report.d.ts +33 -0
  69. package/dist/meta/finetune/report.js +1284 -0
  70. package/dist/meta/finetune/retry.d.ts +24 -0
  71. package/dist/meta/finetune/retry.js +96 -0
  72. package/dist/meta/finetune/sampler.d.ts +18 -0
  73. package/dist/meta/finetune/sampler.js +41 -0
  74. package/dist/meta/finetune/system.d.ts +27 -0
  75. package/dist/meta/finetune/system.js +132 -0
  76. package/dist/meta/finetune/train.d.ts +39 -0
  77. package/dist/meta/finetune/train.js +391 -0
  78. package/dist/meta/finetune/tui.d.ts +16 -0
  79. package/dist/meta/finetune/tui.js +291 -0
  80. package/dist/meta/finetune/tuning.d.ts +167 -0
  81. package/dist/meta/finetune/tuning.js +289 -0
  82. package/dist/meta/finetune/usage.d.ts +62 -0
  83. package/dist/meta/finetune/usage.js +225 -0
  84. package/dist/meta/host.d.ts +14 -0
  85. package/dist/meta/host.js +20 -0
  86. package/dist/meta/index.d.ts +11 -0
  87. package/dist/meta/index.js +11 -0
  88. package/dist/meta/launch.d.ts +81 -0
  89. package/dist/meta/launch.js +286 -0
  90. package/dist/meta/model.d.ts +44 -0
  91. package/dist/meta/model.js +119 -0
  92. package/dist/meta/prompts.d.ts +18 -0
  93. package/dist/meta/prompts.js +61 -0
  94. package/dist/meta/relay.d.ts +20 -0
  95. package/dist/meta/relay.js +123 -0
  96. package/dist/meta/tokens.d.ts +52 -0
  97. package/dist/meta/tokens.js +226 -0
  98. package/dist/meta/window.d.ts +61 -0
  99. package/dist/meta/window.js +198 -0
  100. package/dist/meta/wire.d.ts +42 -0
  101. package/dist/meta/wire.js +129 -0
  102. package/dist/modelref.d.ts +16 -0
  103. package/dist/modelref.js +41 -0
  104. package/dist/narrate.js +5 -2
  105. package/dist/podman.d.ts +42 -0
  106. package/dist/podman.js +198 -5
  107. package/dist/progress.d.ts +33 -0
  108. package/dist/progress.js +402 -0
  109. package/dist/projects.d.ts +0 -5
  110. package/dist/projects.js +2 -15
  111. package/dist/request.d.ts +45 -0
  112. package/dist/request.js +300 -0
  113. package/dist/sandbox.d.ts +2 -0
  114. package/dist/sandbox.js +20 -3
  115. package/dist/scaffold.d.ts +15 -6
  116. package/dist/scaffold.js +5 -60
  117. package/dist/session.d.ts +26 -1
  118. package/dist/session.js +46 -32
  119. package/dist/term.d.ts +5 -0
  120. package/dist/term.js +28 -3
  121. package/dist/trace.d.ts +84 -0
  122. package/dist/trace.js +737 -0
  123. package/dist/tui/models.d.ts +33 -0
  124. package/dist/tui/models.js +389 -0
  125. package/dist/tui/wrap.d.ts +5 -0
  126. package/dist/tui/wrap.js +12 -0
  127. package/dist/usage.d.ts +78 -0
  128. package/dist/usage.js +51 -0
  129. package/dist/validate.js +25 -3
  130. package/package.json +3 -2
  131. package/templates/editor/.github/copilot-instructions.md +41 -107
  132. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  133. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  134. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  135. package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
  136. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  137. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  138. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  139. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  140. package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
  141. package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
  142. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  143. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  144. package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
  145. package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
  146. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  147. package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
  148. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  149. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  150. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  151. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  152. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  153. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  154. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  155. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1791 -0
  156. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  157. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  158. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  159. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  160. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  161. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  162. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  163. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  164. package/templates/editor/.github/skills/zen-inspect/SKILL.md +431 -0
  165. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  166. package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
  167. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
  168. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  169. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  170. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  171. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
  172. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  173. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  174. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  175. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  176. package/templates/editor/.vscode/agents.schema.json +1 -1
  177. package/templates/editor/.vscode/settings.json +2 -0
  178. package/templates/project/agents/files-instructions.md +35 -13
  179. package/templates/project/agents/fork-instructions.md +27 -0
  180. package/templates/project/agents/memory-instructions.md +106 -34
  181. package/templates/project/agents/tools-instructions.md +7 -4
  182. package/templates/project/gitignore +5 -0
  183. package/dist/meta.d.ts +0 -221
  184. package/dist/meta.js +0 -684
@@ -0,0 +1,1791 @@
1
+ ---
2
+ name: zen-finetune
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
+ ---
5
+
6
+ # Fine-tuning an agent project
7
+
8
+ Nothing here touches model weights. What gets tuned is the prose the project is
9
+ made of — the agent prompts, the skills, the house rules in `agents/` — and what
10
+ it is tuned against is a dataset of real queries and the trajectories they
11
+ produce. The model is fixed; the instructions around it are the parameters.
12
+
13
+ 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.
48
+
49
+ ```mermaid
50
+ flowchart TD
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
66
+ end
67
+
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
77
+ end
78
+
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"]
89
+ ```
90
+
91
+ The dataset is built once and cached. Everything after it runs many times.
92
+
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 |
114
+
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.
118
+
119
+ ### What is in a batch
120
+
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
415
+ and the same house rules in both. `--memory-read-only` would share one graph
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:
470
+
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
+ ```
483
+
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.
552
+
553
+ ## What fine-tuning may change
554
+
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.
581
+
582
+ ### The input is whatever the user has
583
+
584
+ A training set arrives as a section of `SPECIFICATION.md`, a markdown file of
585
+ headed examples, a transcript, a spreadsheet exported to CSV, a folder of
586
+ screenshots with captions. There is no format to parse, so do not write a
587
+ parser. Read the file, understand which parts are queries, and write out the
588
+ structure yourself. This is the one step in the loop that is a model's job and
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.
591
+
592
+ | In the source | In the dataset |
593
+ | ---------------------------------------------------------------- | -------------------------------------------------------------------- |
594
+ | A heading like `## Query:` and a quoted string | One case, `input` is the string |
595
+ | A bullet list under `### Rubric:` | `rubric` — **verbatim**, one entry per bullet |
596
+ | An embedded image, a `![](...)` link, an audio or PDF attachment | A media part in `input`, path relative to the dataset file |
597
+ | A paragraph of context before the query | `notes`, not `input` — unless the agent is meant to see it |
598
+ | The expected answer | `expected`, one string; it is context for grading, not a diff target |
599
+ | Nothing but a query | A case with no `rubric`. Still useful, selected after graded ones |
600
+
601
+ Preserve the rubric word for word. A paraphrased rubric grades the paraphrase.
602
+
603
+ ### The shape
604
+
605
+ Write `finetune/dataset.json`:
606
+
607
+ ```json
608
+ {
609
+ "version": 1,
610
+ "source": ["SPECIFICATION.md", "docs/examples/day-planning.md"],
611
+ "extractedAt": "2026-03-04T10:12:00Z",
612
+ "cases": [
613
+ {
614
+ "id": "planning-organize-day",
615
+ "class": "planning",
616
+ "complexity": "medium",
617
+ "input": "organize my day",
618
+ "rubric": [
619
+ "calls api /mail/list",
620
+ "calls api /calendar/list for today",
621
+ "correlates emails with calendar events",
622
+ "proposes a schedule, does not just list both"
623
+ ],
624
+ "expected": "A time-ordered plan naming the two conflicting meetings.",
625
+ "notes": "From SPECIFICATION.md, worked example."
626
+ },
627
+ {
628
+ "id": "receipts-photo-expense",
629
+ "class": "extraction",
630
+ "complexity": "simple",
631
+ "input": ["file this expense", { "image": "./assets/receipt-014.jpg" }],
632
+ "rubric": ["reads the total off the image", "does not invent a vendor"]
633
+ }
634
+ ]
635
+ }
636
+ ```
637
+
638
+ `input` is exactly what `zen run batch` accepts: a string, or an array of parts.
639
+ A part is a string, `{ "text": "…" }`, or `{ "image" | "audio" | "video" | "file": "<path or url>" }`
640
+ with an optional `"mimeType"`. Relative paths resolve against the file they are
641
+ written in — so keep the generated `cases.json` in the same directory tree as the
642
+ media, or write absolute paths. Local media is inlined as base64 and capped at
643
+ 20 MB per part, so reference a URL for anything large.
644
+
645
+ `id` names a directory under the batch dir. Letters, digits, dot, dash and
646
+ underscore only, unique across the dataset, and worth making readable:
647
+ `class-short-slug` means the batch directory listing is already an index.
648
+
649
+ ### Classify only what the source already tells you
650
+
651
+ A class is a distinction the source makes — by section heading, by which agent or
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.
658
+
659
+ ### Rate complexity
660
+
661
+ Three levels, and rate the _trajectory_ the query demands, not the sentence:
662
+
663
+ | Level | What it means |
664
+ | --------- | --------------------------------------------------------------------------- |
665
+ | `simple` | One tool call or none; one agent; the answer is a lookup or a restatement |
666
+ | `medium` | Several calls that depend on each other, or one delegation, or one artefact |
667
+ | `complex` | Fan-out, multi-agent, ambiguity to resolve, or a plan that can go wrong |
668
+
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.
672
+
673
+ ### It is a cache
674
+
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.
681
+
682
+ ## Phase 2 — Select the cases, write the README
683
+
684
+ ### Select
685
+
686
+ ```sh
687
+ .github/skills/zen-finetune/scripts/select.mjs -o finetune/selection.json
688
+ ```
689
+
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)
733
+
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
+ ```
738
+
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` |
747
+
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.
751
+
752
+ ```sh
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"
761
+ ```
762
+
763
+ Then patch the README.
764
+
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.
776
+
777
+ What lands:
778
+
779
+ ```
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
789
+ ```
790
+
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.
793
+
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.
799
+
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:
805
+
806
+ ```sh
807
+ .github/skills/zen-finetune/scripts/report.mjs -d "$RUN/batch" oom
808
+ ```
809
+
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.
822
+
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.
869
+
870
+ ### The three depths
871
+
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 |
877
+
878
+ Stay at the top for as long as it works. The `%%` header rows on the graph carry
879
+ `nodes`, `tokens`, `thinking`, `tools`, `branches`, `compacted` and `why` — and
880
+ the `tools` row is a loop detector: the same tool with the same arguments three
881
+ times is a finding without opening a single node.
882
+
883
+ Descend to `node` for evidence. A finding that does not cite node ids is an
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.
902
+
903
+ ```sh
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"
912
+ ```
913
+
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.
918
+
919
+ ### The criteria
920
+
921
+ Grade each case against all of these. The first is the user's; the rest apply
922
+ whether or not a rubric was written.
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
+
933
+ **1. Rubric compliance.** Every line of the rubric, checked against the graph.
934
+ Mark each `met` / `partial` / `missed` and cite the node. A rubric line naming an
935
+ API call is met by the call appearing with sensible arguments — not by the answer
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
963
+ commit what _produces_ the answer, not the material it was made from: a file
964
+ becomes a pointer, a call becomes the operation. A run that stored the
965
+ _contents_ of `/mail/list` — today's inbox, a price, a status, a queue depth —
966
+ has cached a live reading as if it were a fact, and every later run is wrong.
967
+ What it should have stored is that the mail list is at that endpoint, what shape
968
+ it returns and what the useful filter was. The carve-outs are narrow: an answer
969
+ the agent assembled itself, and a source that genuinely cannot change. Also
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.
973
+
974
+ **4. Fork.** Independent work should fan out; dependent work must not. Look for:
975
+ serial calls with no data dependency between them (missed fork); branches that
976
+ read each other's output (a fork that should have been a sequence); a fork with
977
+ one branch (that is delegation, and should be written as delegation); branches
978
+ that all do the same thing with the same inputs. `branches` in the header and the
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.
981
+
982
+ **5. Delegation.** Did it hand off to the right agent, and did it hand off at
983
+ all? Both failures are common: doing a specialist's job inline because the prompt
984
+ never said to delegate, and delegating a one-line question because the prompt
985
+ said to delegate too eagerly. Check the handoff carries enough context to be
986
+ answerable, and that the result comes back somewhere rather than ending the run.
987
+
988
+ **6. Tools and skills.** Did it use the tool it was given, or hand-roll the same
989
+ thing in a shell? Did it load the skill that covers the task — and if it loaded
990
+ it, did it then follow it? A skill loaded and ignored is a prose problem in the
991
+ skill, not a routing problem.
992
+
993
+ **7. Grounding.** Is every claim in the final answer traceable to something the
994
+ graph shows it read? An answer that is right by luck grades as a failure; it will
995
+ be wrong by luck next time.
996
+
997
+ **8. Failure handling.** When a tool returned an error, did it adapt, or did it
998
+ retry the identical call? The `tools` header row finds this in one line.
999
+
1000
+ ### Write it down: findings.md
1001
+
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.
1006
+
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.
1010
+
1011
+ The table is not typed by hand. `compare` builds it against the previous run and
1012
+ prints markdown ready to paste:
1013
+
1014
+ ```sh
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
1018
+ ```
1019
+
1020
+ Every column comes from the run itself — tokens from `output.json`, llm calls,
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.
1033
+
1034
+ ```md
1035
+ # batch03-nomem-run2 — NO MEMORY, --concurrency 7
1036
+
1037
+ ## PROGRESS — one case fixed, none broken
1038
+
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)
1041
+
1042
+ **Changed since run 1:** one paragraph in `agents/skills/rag_search/SKILL.md`
1043
+ (+11 lines) — a question naming two products is two lookups, and the worst
1044
+ verdict decides.
1045
+
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.
1056
+
1057
+ **Effect:** `info-upgrade-pair-4100-4110` is fixed and nothing regressed. The
1058
+ +121% on it is the second lookup the rule asks for — the cost of the fix.
1059
+
1060
+ **Not the rule's doing:** `code-can-vms-talk` alone is +3.6 M of the +4.5 M, and
1061
+ 124 llm calls against 35 is a loop, not variance — the rule cannot reach that
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.
1065
+
1066
+ machine: concurrency 8 · vm 12.0 GiB / 6 cpus · host 36 GiB
1067
+
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.
1080
+
1081
+ ## planning-organize-day — partial
1082
+
1083
+ | # | Rubric | Verdict | Evidence |
1084
+ | --- | --------------------------------------- | ------- | ----------------------------- |
1085
+ | 1 | calls api /mail/list | met | n7 |
1086
+ | 2 | calls api /calendar/list for today | met | n9 |
1087
+ | 3 | correlates emails with calendar events | partial | n14 — matched on subject only |
1088
+ | 4 | proposes a schedule, does not just list | missed | n21 |
1089
+
1090
+ - optimality: 34 nodes, 61k tokens. n7 and n11 are the same /mail/list call (2)
1091
+ - memory: committed the body of the 09:00 stand-up invite as a fact (3) — n18
1092
+ - fork: n9 and n7 are independent and ran in sequence (4)
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
1101
+ ```
1102
+
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.
1165
+ - **The verdict column decides, not the token column.** Tokens swing ±2× between
1166
+ identical runs of this kind of project. Say what noise floor you measured and
1167
+ treat anything under it as unchanged — an aggregate move of 20% is usually
1168
+ nothing.
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
1178
+ `MIXED`, not a draw — the broken one is the more valuable finding, because it
1179
+ is almost always the new instruction.
1180
+
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.
1183
+
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.
1188
+
1189
+ ### Update the difficult list
1190
+
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:
1194
+
1195
+ ```sh
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
1204
+ ```
1205
+
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:
1255
+
1256
+ - **A prohibition with a self-judged exemption is a permission.** "Do not do X,
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.
1260
+ - **Before rewriting a rule the agent keeps ignoring, grep the whole assembled
1261
+ prefix for the rule's own text.** House rules, the prompt and every preloaded
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-`,
1287
+ `fork-policy-`, `tools-policy-`, `files-policy-instructions.md`. They sit beside
1288
+ `zen`'s own four, they are yours, and `zen check --fix` does not touch them. Load
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`.
1293
+
1294
+ Never edit `agents/memory-instructions.md`, `agents/fork-instructions.md`,
1295
+ `agents/tools-instructions.md` or `agents/files-instructions.md`. They are copies
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.
1299
+
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.
1306
+
1307
+ Then:
1308
+
1309
+ ```sh
1310
+ zen check
1311
+ .github/skills/zen-review/scripts/check-paths.sh
1312
+ ```
1313
+
1314
+ and patch the README. A broken project fails every case for one reason.
1315
+
1316
+ ## Phase 7 — Confirm without memory, build the candidate (steps 7–9)
1317
+
1318
+ Run the **same batch** again as the next run — a copy of its cases file,
1319
+ never a rebuild — still with no memory:
1320
+
1321
+ ```sh
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
1329
+ ```
1330
+
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.
1346
+
1347
+ Three things to watch across the whole tuning:
1348
+
1349
+ - **The score going up for the wrong reason.** If the new rule names the
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:
1364
+
1365
+ ```sh
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
+ ```
1369
+
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"
1418
+ ```
1419
+
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.
1429
+
1430
+ Five things follow from copying rather than sharing:
1431
+
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.
1444
+ - **Copies cost disk and a pause.** They are made serially, before the first
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.
1447
+ - **The source must not be locked.** `zen run batch` refuses a memory another
1448
+ process holds, so close any `zen memory` session first.
1449
+
1450
+ ### Analyze the memory use (step 11)
1451
+
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:
1455
+
1456
+ ```sh
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
1464
+ ```
1465
+
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.
1470
+
1471
+ | Good | Bad |
1472
+ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
1473
+ | Far fewer nodes and tokens than without memory, same verdict | Same trajectory — memory was never consulted |
1474
+ | Recall replaces a discovery step: it knows the endpoint without probing | Recall replaces a _reading_: it reports yesterday's inbox as today's |
1475
+ | One recall near the top, then straight to work | Repeated searches of memory mid-run, finding nothing |
1476
+ | Recalled facts are checked when they are cheap to check | A recalled fact contradicts a fresh reading and the recall wins |
1477
+ | Nothing is committed that memory already held — `known` is 0 | A near-copy of a node it recalled this run, committed again |
1478
+ | A fact found to be wrong is superseded, not merely added to | The correction sits beside the stale node with no SUPERSEDES |
1479
+ | The answer is at least as good | The answer got worse — a stale fact short-circuited the work |
1480
+
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:
1550
+
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`.
1558
+
1559
+ Write `PASSED`, then keep the candidate before anything else:
1560
+
1561
+ ```sh
1562
+ .github/skills/zen-finetune/scripts/memory.mjs checkpoint batch01-mem-run1
1563
+ ```
1564
+
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.
1569
+
1570
+ ## The final check
1571
+
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.
1578
+
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.
1599
+
1600
+ ## The state directory
1601
+
1602
+ ```
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
1623
+ batch/ zen run batch output
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
1632
+ ```
1633
+
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`:
1646
+
1647
+ ```
1648
+ finetune/runs/*/batch/
1649
+ finetune/memory/
1650
+ finetune/empty/
1651
+ finetune/usage/
1652
+ ```
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
+
1669
+ ## Scripts this skill ships
1670
+
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 |
1680
+
1681
+ The README template is `references/readme-template.md`.
1682
+
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/`.
1688
+
1689
+ ## Rules that are easy to get wrong
1690
+
1691
+ 1. **Never give a no-memory run memory.** A run with no memory is the only one
1692
+ whose result is attributable to the prose.
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.
1699
+ 4. **Never tune with `--memory-read-only`.** It clamps `access` to `read`, which
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
1740
+ graph, it did not happen.
1741
+ 18. **Never let a rule name the dataset.** Endpoints and entity names from the
1742
+ training set inside an instruction means the eval passes and the product does
1743
+ not.
1744
+ 19. **Run `zen check` after every round of edits.** A load error fails every case
1745
+ identically and looks exactly like a catastrophic regression.
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.
1763
+
1764
+ ## When not to fine-tune
1765
+
1766
+ - **The project does not load.** `zen check` first; a run on a broken project
1767
+ measures nothing. Load **zen-review**.
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.
1771
+ - **There is no training set.** Fewer than about eight queries is a debugging
1772
+ session, not a tuning loop — run them singly with `zen run` and read the graphs.
1773
+ - **One run is behaving strangely.** That is diagnosis. Load **zen-analyze-run**
1774
+ and analyze that run.
1775
+ - **The specification changed.** Reconciling prose with intent is
1776
+ **zen-spec-sync**'s job; tune after it, against what the project is meant to be.
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.
1781
+
1782
+ ## Related
1783
+
1784
+ - **zen-cli** — command surface, flags, exit codes, and what `--json` emits
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
1787
+ - **zen-instructions** — house rules, `requires:`, filename order, `zen check`
1788
+ - **zen-memory** — the graph model, commit rules, audiences, `merge` and `stats`
1789
+ - **zen-memory-warmup** — building a memory deliberately, and shipping one
1790
+ - **zen-review** — the mechanical checks to run before and after every batch
1791
+ - **zen-spec-sync** — reconciling the project with its specification, before tuning