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