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