@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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
This is the template for finetune/README.md. It is not generated: the agent
|
|
3
|
+
writes it once, before the first run, and patches it after every step.
|
|
4
|
+
|
|
5
|
+
Copy it, replace every <angle-bracket> placeholder with the real value, and
|
|
6
|
+
delete this comment. The example numbers below (64 cases, all 64 used, batches
|
|
7
|
+
growing 3 -> 6 -> 10, recheck 3) are there to show the shape - use the
|
|
8
|
+
project's own. The limit is the whole dataset unless the user named a smaller
|
|
9
|
+
number.
|
|
10
|
+
|
|
11
|
+
What gets patched after each step is listed in the SKILL, "The README".
|
|
12
|
+
-->
|
|
13
|
+
|
|
14
|
+
# Fine-tuning <project name>
|
|
15
|
+
|
|
16
|
+
<One paragraph, for someone who has never seen this project: what is being
|
|
17
|
+
tuned, against which training set, and why. E.g. "This tunes the prompts and
|
|
18
|
+
skills of the support-desk agents against the 64 example questions in
|
|
19
|
+
SPECIFICATION.md §4. Nothing touches the model; what changes is the text the
|
|
20
|
+
agents are given.">
|
|
21
|
+
|
|
22
|
+
## Where we are
|
|
23
|
+
|
|
24
|
+
<!-- Patched after EVERY step. Two or three lines, plain words. -->
|
|
25
|
+
|
|
26
|
+
**Batch 3 (10 new + 3 recheck) · without memory · run 1 - waiting to be graded.**
|
|
27
|
+
Batches 1-2 passed, both halves. 9 of 64 cases used, 55 to go; 1 difficult case open.
|
|
28
|
+
|
|
29
|
+
## The plan
|
|
30
|
+
|
|
31
|
+
<!-- Written once, before the first run. Only changed if the numbers change,
|
|
32
|
+
and then say so under "Log". -->
|
|
33
|
+
|
|
34
|
+
### What we are testing against
|
|
35
|
+
|
|
36
|
+
The training set holds **64 cases** (the dataset). Every one of them was read
|
|
37
|
+
into `dataset.json`; none was dropped.
|
|
38
|
+
|
|
39
|
+
This tuning uses **all 64** of them (limit 64 of 64), in an order where the
|
|
40
|
+
classes take turns, so every kind of question is worked on from the start, not
|
|
41
|
+
just the most common - and inside each class the cases with a rubric and the
|
|
42
|
+
most complex ones come first, because they show the most per run:
|
|
43
|
+
|
|
44
|
+
| class | in the dataset | used | with a rubric | complex |
|
|
45
|
+
| ---------- | -------------: | -----: | ------------: | ------: |
|
|
46
|
+
| planning | 22 | 22 | 15 | 8 |
|
|
47
|
+
| extraction | 18 | 18 | 7 | 4 |
|
|
48
|
+
| search | 14 | 14 | 12 | 5 |
|
|
49
|
+
| billing | 10 | 10 | 5 | 2 |
|
|
50
|
+
| **total** | **64** | **64** | **39** | **19** |
|
|
51
|
+
|
|
52
|
+
### How the 64 are worked through: batches
|
|
53
|
+
|
|
54
|
+
We do not run all 64 and then try to fix everything at once - after one big run
|
|
55
|
+
there is too much to read, and after one big edit nobody can say which change
|
|
56
|
+
helped. Instead they are worked through a **batch** at a time.
|
|
57
|
+
|
|
58
|
+
**Batches start small and grow.** Batch 1 is **3 cases** - a smoke test that the
|
|
59
|
+
project, the sandbox and the grading all work. Each batch that goes smoothly
|
|
60
|
+
(at most 2 runs of each kind, no case added to the difficult list) makes the
|
|
61
|
+
next one **twice as big**, up to **10**; a batch that struggles keeps the next
|
|
62
|
+
one the same size. Each batch also re-runs **3 recheck cases** from earlier
|
|
63
|
+
batches - difficult ones first, then complex ones - to catch a fix that broke
|
|
64
|
+
something.
|
|
65
|
+
|
|
66
|
+
**Every batch goes through the same steps, first without memory, then with it:**
|
|
67
|
+
|
|
68
|
+
1. **Run** the batch with an empty memory - a right answer is then the
|
|
69
|
+
instructions' doing, not something remembered.
|
|
70
|
+
2. **Grade** every run: is the answer right? What did it cost - how many model
|
|
71
|
+
calls, could independent steps have run in parallel, did it explore for
|
|
72
|
+
things it could have been told? What did it save to memory? Where it went
|
|
73
|
+
wrong, ask the model why.
|
|
74
|
+
3. **Collect** the improvements each case suggests, **merge** them into
|
|
75
|
+
patterns, and **fix** the prompts, skills or rules they point at.
|
|
76
|
+
4. **Run the same cases again** to confirm. The questions are identical and only
|
|
77
|
+
the instructions changed, so the difference is the fix. Repeat until every
|
|
78
|
+
case is right and nothing is left worth cutting - at most 4 runs.
|
|
79
|
+
5. **Build the batch's memory**: everything the cases saved in that passing run,
|
|
80
|
+
added to the memory of the earlier batches.
|
|
81
|
+
6. **Run the same cases with that memory** - each case gets its own copy. The
|
|
82
|
+
answers should not change, but the work should get **much cheaper**: what
|
|
83
|
+
was rediscovered every time is now remembered - and nothing it already knew
|
|
84
|
+
should be saved again.
|
|
85
|
+
7. **Grade the memory use**, fix the memory rules, run again - at most 4 runs.
|
|
86
|
+
8. When that passes, the batch's memory becomes the **last good memory**, and
|
|
87
|
+
the next batch is built.
|
|
88
|
+
|
|
89
|
+
```mermaid
|
|
90
|
+
flowchart LR
|
|
91
|
+
SEL[("the 64 cases")] -->|"3, then 6, then 10 not used yet"| NM
|
|
92
|
+
DIF[("difficult list")] -->|"1. open ones first"| RC
|
|
93
|
+
PREV[("cases from<br/>earlier batches")] -->|"2. then complex ones"| RC["3 recheck cases"]
|
|
94
|
+
RC --> NM["run without memory"]
|
|
95
|
+
NM --> G1{"right, and<br/>nothing to cut?"}
|
|
96
|
+
G1 -->|no| FIX["merge proposals<br/>fix prompts / skills / rules"] --> NM
|
|
97
|
+
G1 -->|yes| CAND["candidate memory:<br/>last good + what it saved"]
|
|
98
|
+
CAND --> WM["run with memory<br/>a copy per case"]
|
|
99
|
+
WM --> G2{"same answers,<br/>much cheaper,<br/>nothing saved twice?"}
|
|
100
|
+
G2 -->|no| MFIX["fix the memory rules"] --> WM
|
|
101
|
+
G2 -->|yes| KEEP["it becomes the<br/>last good memory"] --> NEXT["next batch"]
|
|
102
|
+
G1 -.->|"wrong again · regressed · flaky · costly"| DIF
|
|
103
|
+
G2 -.->|"memory made it worse"| DIF
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
64 cases growing 3 → 6 → 10 is **at least 8 batches**: 3 + 6 + 10 × 5 + 5.
|
|
107
|
+
|
|
108
|
+
### Difficult cases keep coming back
|
|
109
|
+
|
|
110
|
+
Some cases cause most of the trouble: still wrong after a fix, breaking again
|
|
111
|
+
as a recheck case, made worse by memory, or far more expensive than the rest.
|
|
112
|
+
They go on a **difficult list** and are not forgotten when their batch passes:
|
|
113
|
+
|
|
114
|
+
- they are the **first recheck cases** in every later batch until fixed, and
|
|
115
|
+
still preferred after that;
|
|
116
|
+
- once all 64 have been used, the next batches hold **only the open difficult
|
|
117
|
+
cases** - so 8 batches may become 10 or 11;
|
|
118
|
+
- a case still wrong after 3 more batches is marked **stuck** and reported,
|
|
119
|
+
rather than retried forever.
|
|
120
|
+
|
|
121
|
+
The tuning ends when all 64 have been used and no difficult case is open. A
|
|
122
|
+
**final check** then runs all 64 at once, with the last good memory and nothing
|
|
123
|
+
changed.
|
|
124
|
+
|
|
125
|
+
### Nothing learned is lost
|
|
126
|
+
|
|
127
|
+
The last good memory is only ever replaced by a complete one. If the tuning
|
|
128
|
+
stops at any point, it holds everything the passed batches learned, and is the
|
|
129
|
+
memory to ship.
|
|
130
|
+
|
|
131
|
+
### Settings
|
|
132
|
+
|
|
133
|
+
| setting | value | meaning |
|
|
134
|
+
| ----------- | ----: | ------------------------------------------------------------------------------- |
|
|
135
|
+
| limit | 64 | every case in the dataset |
|
|
136
|
+
| minBatch | 3 | new cases in batch 1, the smoke test |
|
|
137
|
+
| maxBatch | 10 | the most new cases a batch grows to |
|
|
138
|
+
| recheck | 3 | earlier cases re-run in each batch, difficult then complex first |
|
|
139
|
+
| concurrency | 13 | most cases running at the same time: min(maxBatch + recheck, 16), halved on OOM |
|
|
140
|
+
| seed | 1 | makes the choice of rechecks repeatable |
|
|
141
|
+
| maxRetries | 3 | batches a difficult case gets before it is stuck |
|
|
142
|
+
|
|
143
|
+
## Map
|
|
144
|
+
|
|
145
|
+
<!-- Patched after EVERY step: move node ids between the `class` lines, and
|
|
146
|
+
update labels (runs so far, ✔ / ✘). `now` is exactly one node. Each batch
|
|
147
|
+
is two nodes, its no-memory half (xN) and its with-memory half (xM). The
|
|
148
|
+
batches the growth rule gives if every batch is smooth are drawn ahead;
|
|
149
|
+
when a batch stays small, or difficult cases add batches, change the sizes
|
|
150
|
+
ahead and ADD nodes. Keep the "left" line under the map current. -->
|
|
151
|
+
|
|
152
|
+
```mermaid
|
|
153
|
+
flowchart TD
|
|
154
|
+
DS["dataset<br/>64 cases"] --> SEL["selection<br/>all 64"]
|
|
155
|
+
DIF[("difficult list<br/>1 open · 0 fixed · 0 stuck")]
|
|
156
|
+
MEM[("last good memory<br/>batches 1-2 · 41 nodes")]
|
|
157
|
+
|
|
158
|
+
subgraph B1["batch 1 · 3 new"]
|
|
159
|
+
direction LR
|
|
160
|
+
A1N["no memory<br/>✔ 2 runs"] --> A1M["with memory<br/>✔ 1 run · −58% tokens"]
|
|
161
|
+
end
|
|
162
|
+
subgraph B2["batch 2 · 6 new + 3"]
|
|
163
|
+
direction LR
|
|
164
|
+
A2N["no memory<br/>✔ 1 run"] --> A2M["with memory<br/>✔ 2 runs · −61% tokens"]
|
|
165
|
+
end
|
|
166
|
+
subgraph B3["batch 3 · 10 new + 3"]
|
|
167
|
+
direction LR
|
|
168
|
+
A3N["no memory<br/>▶ run 1"] --> A3M["with memory"]
|
|
169
|
+
end
|
|
170
|
+
subgraph REST["batches 4-8 · 10, 10, 10, 10, 5 new"]
|
|
171
|
+
direction LR
|
|
172
|
+
A4["batch 4"] --> A5["batch 5"] --> A6["batch 6"] --> A7["batch 7"] --> A8["batch 8"]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
SEL --> A1N
|
|
176
|
+
A1M --> A2N
|
|
177
|
+
A2M --> A3N
|
|
178
|
+
A3M --> A4
|
|
179
|
+
A1M -.->|"kept"| MEM
|
|
180
|
+
A2M -.-> MEM
|
|
181
|
+
A2N -.->|"planning-organize-day"| DIF
|
|
182
|
+
DIF -.->|"first recheck of every later batch"| A3N
|
|
183
|
+
MEM -.->|"copied into the candidate"| A3M
|
|
184
|
+
A8 --> FINAL["final check<br/>all 64 at once"] --> DONE(["done"])
|
|
185
|
+
|
|
186
|
+
classDef done fill:#14532d,stroke:#22c55e,color:#fff
|
|
187
|
+
classDef now fill:#713f12,stroke:#eab308,color:#fff
|
|
188
|
+
classDef failed fill:#7f1d1d,stroke:#ef4444,color:#fff
|
|
189
|
+
classDef ahead fill:#2b2b2b,stroke:#666,color:#bbb
|
|
190
|
+
|
|
191
|
+
class DS,SEL,A1N,A1M,A2N,A2M,MEM,DIF done
|
|
192
|
+
class A3N now
|
|
193
|
+
class A3M,A4,A5,A6,A7,A8,FINAL,DONE ahead
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Left:** 55 unused cases (at least 6 more batches), 1 difficult case open.
|
|
197
|
+
|
|
198
|
+
## Batches
|
|
199
|
+
|
|
200
|
+
<!-- One row per batch, patched when a run finishes or is graded. "without"
|
|
201
|
+
is the no-memory runs, first to last; "with" is the with-memory run that
|
|
202
|
+
passed, against the no-memory run that passed. -->
|
|
203
|
+
|
|
204
|
+
| batch | size | new + recheck | runs (no mem / mem) | status | cases right | llm calls without | llm calls with | tokens with vs without | saved twice | what changed |
|
|
205
|
+
| ----- | ---: | ---------------------- | ------------------: | --------- | ----------- | ----------------- | -------------- | ---------------------- | ----------: | ------------------------------------------------- |
|
|
206
|
+
| 1 | 3 | `<3 ids>` | 2 / 1 | ✔ passed | 3/3 + 0/0 | 96 → 71 | 31 | −58% | 0 | [2 rules](runs/batch01-nomem-run1/changes.md) |
|
|
207
|
+
| 2 | 6 | `<6 ids>` + `<3 ids>` | 1 / 2 | ✔ passed | 6/6 + 3/3 | 240 | 98 | −61% | 4 → 0 | [1 memory rule](runs/batch02-mem-run1/changes.md) |
|
|
208
|
+
| 3 | 10 | `<10 ids>` + `<3 ids>` | 1 / - | ▶ grading | - | - | - | - | - | - |
|
|
209
|
+
|
|
210
|
+
"cases right" is new + recheck, e.g. `9/10 + 3/3`. "saved twice" is how many
|
|
211
|
+
memories a with-memory run saved that it already had - the target is 0.
|
|
212
|
+
|
|
213
|
+
## Difficult cases
|
|
214
|
+
|
|
215
|
+
<!-- Patched whenever difficult.mjs changes. Copy its list; add what was tried. -->
|
|
216
|
+
|
|
217
|
+
| case | why | status | retries | what goes wrong, and what was tried |
|
|
218
|
+
| ----------------------- | ------ | ------ | ------: | ------------------------------------------------------------------------------------- |
|
|
219
|
+
| `planning-organize-day` | wrong | open | 0 | lists mail and calendar, never proposes a schedule; planner rule 1 did not move it |
|
|
220
|
+
| `billing-refund-late` | costly | fixed | 1 | 90 llm calls reading every policy file; fixed by naming the file in the billing skill |
|
|
221
|
+
|
|
222
|
+
Last good memory: 41 nodes, from batches 1-2 (`memory.mjs`).
|
|
223
|
+
|
|
224
|
+
## Log
|
|
225
|
+
|
|
226
|
+
<!-- Newest first. One entry per run, added when it is graded; a line when the
|
|
227
|
+
batch passes. Link the run's findings.md and changes.md. -->
|
|
228
|
+
|
|
229
|
+
### batch02-mem-run2 - PASSED
|
|
230
|
+
|
|
231
|
+
- Same 9 cases, with memory, after 1 memory rule.
|
|
232
|
+
- Same answers as without memory; tokens −61%, llm calls 240 → 98.
|
|
233
|
+
Nothing saved twice (run 1: 4).
|
|
234
|
+
- Batch 2 passed; its memory is now the last good memory. Batch 3 grows to 10.
|
|
235
|
+
- [findings](runs/batch02-mem-run2/findings.md)
|
|
236
|
+
|
|
237
|
+
### batch02-mem-run1 - CORRECT
|
|
238
|
+
|
|
239
|
+
- First run of batch 2 with memory. All right, −55% tokens, but 4 cases saved
|
|
240
|
+
again the endpoint they had just recalled.
|
|
241
|
+
- Changed: 1 rule in the memory policy - [changes](runs/batch02-mem-run1/changes.md).
|
|
242
|
+
- [findings](runs/batch02-mem-run1/findings.md)
|
|
243
|
+
|
|
244
|
+
## Results so far
|
|
245
|
+
|
|
246
|
+
<!-- Patched when a batch passes. -->
|
|
247
|
+
|
|
248
|
+
- **Cases passing:** 9 of 64 (batches 1-2).
|
|
249
|
+
- **Instructions changed:** `agents/prompts/planner.md` (+6 lines),
|
|
250
|
+
`agents/skills/billing/SKILL.md` (+4 lines),
|
|
251
|
+
`agents/memory-policy-instructions.md` (+5 lines).
|
|
252
|
+
- **Memory:** 41 nodes; with memory, batches run 58-61% cheaper with the same
|
|
253
|
+
answers.
|
|
254
|
+
- **Difficult cases:** 1 open, 1 fixed, 0 stuck.
|
|
255
|
+
- **Open problems:** none yet.
|
|
256
|
+
- **Tokens:** [USAGE.md](USAGE.md) - by model, batch and run; written by
|
|
257
|
+
`usage.mjs`, never by hand.
|
|
258
|
+
|
|
259
|
+
## Next
|
|
260
|
+
|
|
261
|
+
<!-- Patched after EVERY step. What `next.mjs` says, in words, then the command. -->
|
|
262
|
+
|
|
263
|
+
Grade batch 3's first run without memory: check it was not killed for memory,
|
|
264
|
+
then read every trajectory.
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
.github/skills/zen-finetune/scripts/report.mjs -d finetune/runs/batch03-nomem-run1/batch oom
|
|
268
|
+
```
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
//
|
|
4
|
+
// Build the next batch: the cases the next run of the tuning will work on.
|
|
5
|
+
//
|
|
6
|
+
// There is no plan of batches drawn up in advance. Each batch is decided when it
|
|
7
|
+
// is needed, from what has happened so far:
|
|
8
|
+
//
|
|
9
|
+
// size batch 1 takes minBatch new cases - a smoke test of the project and
|
|
10
|
+
// the loop. Each later batch doubles the size of the one before, up
|
|
11
|
+
// to maxBatch, when that one went smoothly: at most 2 no-memory runs,
|
|
12
|
+
// at most 2 with-memory runs, and no case put on the difficult list.
|
|
13
|
+
// Otherwise it keeps the size it had.
|
|
14
|
+
// new that many cases from the selection that no batch has used, in
|
|
15
|
+
// selection order - classes in turn, rubric and complex cases first
|
|
16
|
+
// recheck up to `recheck` cases from earlier batches: open difficult cases
|
|
17
|
+
// first, then fixed difficult ones, then complex cases, then the
|
|
18
|
+
// rest - each group in a seeded random order
|
|
19
|
+
//
|
|
20
|
+
// When the selection is used up, a batch is made of open difficult cases only,
|
|
21
|
+
// up to the size, plus the rechecks. So the whole selection is covered and the
|
|
22
|
+
// cases that caused trouble keep coming back until they are fixed, or `stuck`
|
|
23
|
+
// after `maxRetries` further batches. When nothing new and nothing open is left,
|
|
24
|
+
// the tuning goes to the final check.
|
|
25
|
+
//
|
|
26
|
+
// It writes the batch's cases.json, and plan.json beside it: the size, which
|
|
27
|
+
// cases are new, which are rechecks, and why the size is what it is. A batch is
|
|
28
|
+
// built once, for its first no-memory run; every later run of it - no memory or
|
|
29
|
+
// with memory - gets a copy of that cases.json, because the difficult list - and
|
|
30
|
+
// so the rechecks - moves.
|
|
31
|
+
//
|
|
32
|
+
// Run it from anywhere; it finds the project root from its own location.
|
|
33
|
+
// See .github/skills/zen-finetune/SKILL.md.
|
|
34
|
+
|
|
35
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { dirname, join, resolve } from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
|
|
39
|
+
const NAME = 'batch.mjs';
|
|
40
|
+
|
|
41
|
+
const USAGE = `Build the next batch: the cases the next run of the tuning will work on.
|
|
42
|
+
|
|
43
|
+
${NAME} progress: cases used, left, difficult open
|
|
44
|
+
${NAME} next -o <run>/cases.json the next batch, and <run>/plan.json beside it
|
|
45
|
+
|
|
46
|
+
-m <size> new cases in this batch, overriding the growth rule
|
|
47
|
+
-r <count> recheck cases per batch; default config.json "recheck", else 3
|
|
48
|
+
-o <file> where to write; default stdout (and no plan.json)
|
|
49
|
+
--seed <n> default config.json "seed", else 1`;
|
|
50
|
+
|
|
51
|
+
process.stdout.on('error', (err) => {
|
|
52
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'EPIPE') {
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
process.chdir(resolve(dirname(fileURLToPath(import.meta.url)), '../../../..'));
|
|
58
|
+
|
|
59
|
+
const CONFIG = 'finetune/config.json';
|
|
60
|
+
const SELECTION = 'finetune/selection.json';
|
|
61
|
+
const DATASET = 'finetune/dataset.json';
|
|
62
|
+
const DIFFICULT = 'finetune/difficult.json';
|
|
63
|
+
const RUNS = 'finetune/runs';
|
|
64
|
+
const RUN_NAME = /^batch(\d{2})-(nomem|mem)-run(\d+)$/;
|
|
65
|
+
const SEVERITY = ['wrong', 'regressed', 'flaky', 'memory', 'costly'];
|
|
66
|
+
// A batch that needed more runs than this, of either kind, does not grow the next one.
|
|
67
|
+
const SMOOTH_RUNS = 2;
|
|
68
|
+
|
|
69
|
+
/** @param {string} msg @param {number} [code] @returns {never} */
|
|
70
|
+
function die(msg, code = 2) {
|
|
71
|
+
console.error(`${NAME}: ${msg}`);
|
|
72
|
+
process.exit(code);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @param {string} path @returns {any} */
|
|
76
|
+
function json(path) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Arguments
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
let mode = 'status';
|
|
89
|
+
/** @type {number | undefined} */
|
|
90
|
+
let size;
|
|
91
|
+
/** @type {number | undefined} */
|
|
92
|
+
let recheck;
|
|
93
|
+
/** @type {number | undefined} */
|
|
94
|
+
let seed;
|
|
95
|
+
let out = '';
|
|
96
|
+
|
|
97
|
+
const argv = process.argv.slice(2);
|
|
98
|
+
/** @param {string} flag @returns {string} */
|
|
99
|
+
const value = (flag) => argv.shift() ?? die(`${flag} needs a value`);
|
|
100
|
+
/** @param {string} flag @returns {number} */
|
|
101
|
+
const whole = (flag) => {
|
|
102
|
+
const raw = value(flag);
|
|
103
|
+
if (!/^\d+$/.test(raw)) {
|
|
104
|
+
die(`${flag} takes a whole number`);
|
|
105
|
+
}
|
|
106
|
+
return Number(raw);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
while (argv.length > 0) {
|
|
110
|
+
const arg = /** @type {string} */ (argv.shift());
|
|
111
|
+
switch (arg) {
|
|
112
|
+
case 'next':
|
|
113
|
+
mode = 'next';
|
|
114
|
+
break;
|
|
115
|
+
case '-m':
|
|
116
|
+
size = whole('-m');
|
|
117
|
+
break;
|
|
118
|
+
case '-r':
|
|
119
|
+
recheck = whole('-r');
|
|
120
|
+
break;
|
|
121
|
+
case '-o':
|
|
122
|
+
out = value('-o');
|
|
123
|
+
break;
|
|
124
|
+
case '--seed':
|
|
125
|
+
seed = whole('--seed');
|
|
126
|
+
break;
|
|
127
|
+
case '-h':
|
|
128
|
+
case '--help':
|
|
129
|
+
console.log(USAGE);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
console.error(`${NAME}: unknown argument ${arg}`);
|
|
134
|
+
console.error(USAGE);
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @type {{ id: string }[]} */
|
|
140
|
+
const selection = json(SELECTION)?.batch ?? die(`no ${SELECTION} - run select.mjs first`);
|
|
141
|
+
|
|
142
|
+
const config = json(CONFIG) ?? {};
|
|
143
|
+
/** @param {string} key @param {number} fallback @returns {number} */
|
|
144
|
+
const cfg = (key, fallback) => {
|
|
145
|
+
const n = Number(config[key]);
|
|
146
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
147
|
+
};
|
|
148
|
+
const MIN = cfg('minBatch', 3);
|
|
149
|
+
const MAX = Math.max(MIN, cfg('maxBatch', 10));
|
|
150
|
+
const RECHECK = recheck ?? (Number.isFinite(Number(config.recheck)) ? Number(config.recheck) : 3);
|
|
151
|
+
const SEED = seed ?? cfg('seed', 1);
|
|
152
|
+
const MAX_RETRIES = cfg('maxRetries', 3);
|
|
153
|
+
|
|
154
|
+
if (size !== undefined && size < 1) {
|
|
155
|
+
die('-m must be at least 1');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @type {Map<string, string>} */
|
|
159
|
+
const complexity = new Map(
|
|
160
|
+
(json(DATASET)?.cases ?? json(DATASET)?.samples ?? []).map(
|
|
161
|
+
(/** @type {{ id: string, complexity?: string }} */ c) => [c.id, c.complexity ?? ''],
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// What has happened so far
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/** @type {string[]} */
|
|
170
|
+
const names = (() => {
|
|
171
|
+
try {
|
|
172
|
+
return readdirSync(RUNS);
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
|
|
178
|
+
/** @param {string} dir @returns {string} the verdict heading of a run's findings.md */
|
|
179
|
+
function verdict(dir) {
|
|
180
|
+
const path = join(dir, 'findings.md');
|
|
181
|
+
const match = existsSync(path)
|
|
182
|
+
? /^## ([A-Z][A-Z ]*[A-Z])/m.exec(readFileSync(path, 'utf8'))
|
|
183
|
+
: null;
|
|
184
|
+
return match ? match[1].trimEnd() : '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The case ids of every batch, by batch number, read off its first no-memory run.
|
|
189
|
+
*
|
|
190
|
+
* @type {Map<number, string[]>}
|
|
191
|
+
*/
|
|
192
|
+
const done = new Map();
|
|
193
|
+
for (const name of names) {
|
|
194
|
+
const m = RUN_NAME.exec(name);
|
|
195
|
+
if (m && m[2] === 'nomem' && +m[3] === 1) {
|
|
196
|
+
const ids = json(join(RUNS, name, 'cases.json'))?.batch?.map(
|
|
197
|
+
(/** @type {{ id: string }} */ c) => c.id,
|
|
198
|
+
);
|
|
199
|
+
if (ids) {
|
|
200
|
+
done.set(+m[1], ids);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const number = done.size === 0 ? 1 : Math.max(...done.keys()) + 1;
|
|
206
|
+
const used = new Set([...done.values()].flat());
|
|
207
|
+
const pad = (/** @type {number} */ n) => String(n).padStart(2, '0');
|
|
208
|
+
|
|
209
|
+
/** @param {string} run @returns {number} */
|
|
210
|
+
const batchOf = (run) => Number(RUN_NAME.exec(run)?.[1] ?? 0);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @typedef {{ id: string, reasons: string[], added: string, fixed?: string }} Entry
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
/** How many batches since it was added have held it; `maxRetries` of them is stuck. */
|
|
217
|
+
const retries = (/** @type {Entry} */ e) =>
|
|
218
|
+
[...done.entries()].filter(([n, ids]) => n > batchOf(e.added) && ids.includes(e.id)).length;
|
|
219
|
+
|
|
220
|
+
/** @type {Entry[]} */
|
|
221
|
+
const listed = json(DIFFICULT)?.cases ?? [];
|
|
222
|
+
const worst = (/** @type {Entry} */ e) => Math.min(...e.reasons.map((r) => SEVERITY.indexOf(r)));
|
|
223
|
+
const open = listed
|
|
224
|
+
.filter((e) => !e.fixed && retries(e) < MAX_RETRIES)
|
|
225
|
+
.sort((a, b) => worst(a) - worst(b) || retries(a) - retries(b) || a.id.localeCompare(b.id));
|
|
226
|
+
const fixed = listed.filter((e) => e.fixed);
|
|
227
|
+
const stuck = listed.filter((e) => !e.fixed && retries(e) >= MAX_RETRIES);
|
|
228
|
+
|
|
229
|
+
const unused = selection.filter((c) => !used.has(c.id));
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The size of batch `n`, and why. Batch 1 is the smoke test. A later batch
|
|
233
|
+
* doubles the one before when that one went smoothly, and keeps its size when
|
|
234
|
+
* it did not.
|
|
235
|
+
*
|
|
236
|
+
* @param {number} n
|
|
237
|
+
* @returns {{ size: number, why: string }}
|
|
238
|
+
*/
|
|
239
|
+
function sizeOf(n) {
|
|
240
|
+
if (n === 1) {
|
|
241
|
+
return { size: MIN, why: `batch 1 is the smoke test: minBatch ${MIN}` };
|
|
242
|
+
}
|
|
243
|
+
const prev = n - 1;
|
|
244
|
+
const plan = json(join(RUNS, `batch${pad(prev)}-nomem-run1`, 'plan.json'));
|
|
245
|
+
const was = Number(plan?.size) || MIN;
|
|
246
|
+
/** @param {string} kind */
|
|
247
|
+
const runs = (kind) =>
|
|
248
|
+
names.filter((name) => {
|
|
249
|
+
const m = RUN_NAME.exec(name);
|
|
250
|
+
return m && +m[1] === prev && m[2] === kind && verdict(join(RUNS, name)) !== 'VOID';
|
|
251
|
+
}).length;
|
|
252
|
+
const nomem = runs('nomem');
|
|
253
|
+
const mem = runs('mem');
|
|
254
|
+
const added = listed.filter((e) => batchOf(e.added) === prev && !e.fixed).length;
|
|
255
|
+
if (nomem <= SMOOTH_RUNS && mem <= SMOOTH_RUNS && added === 0) {
|
|
256
|
+
const grown = Math.min(MAX, was * 2);
|
|
257
|
+
return {
|
|
258
|
+
size: grown,
|
|
259
|
+
why:
|
|
260
|
+
grown > was
|
|
261
|
+
? `batch ${prev} went smoothly (${nomem} no-memory + ${mem} with-memory runs, no new difficult case): ${was} -> ${grown}`
|
|
262
|
+
: `batch ${prev} went smoothly; already at maxBatch ${MAX}`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
size: was,
|
|
267
|
+
why: `batch ${prev} did not go smoothly (${nomem} no-memory + ${mem} with-memory runs, ${added} difficult case(s) added and open): stays at ${was}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const planned = size !== undefined ? { size, why: `set by -m ${size}` } : sizeOf(number);
|
|
272
|
+
|
|
273
|
+
if (mode === 'status') {
|
|
274
|
+
console.log(`selection ${selection.length} cases`);
|
|
275
|
+
console.log(`used ${used.size}, ${unused.length} not yet in any batch`);
|
|
276
|
+
console.log(`batches ${done.size} built`);
|
|
277
|
+
console.log(`next size ${planned.size} - ${planned.why}`);
|
|
278
|
+
console.log(
|
|
279
|
+
`difficult ${open.length} open · ${fixed.length} fixed · ${stuck.length} stuck`,
|
|
280
|
+
);
|
|
281
|
+
console.log(
|
|
282
|
+
`ahead at least ${Math.ceil(unused.length / MAX)} more batch(es) for new cases` +
|
|
283
|
+
(open.length ? `, plus what the ${open.length} open difficult case(s) need` : ''),
|
|
284
|
+
);
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// The next batch
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* `n` successive Lehmer values, s * 48271 mod 2^31-1: exact in a double, so the
|
|
294
|
+
* same draw on every machine.
|
|
295
|
+
*
|
|
296
|
+
* @param {number} start
|
|
297
|
+
* @param {number} n
|
|
298
|
+
*/
|
|
299
|
+
function keys(start, n) {
|
|
300
|
+
const out = [];
|
|
301
|
+
let s = start;
|
|
302
|
+
for (let i = 0; i < n; i++) {
|
|
303
|
+
s = (s * 48271) % 2147483647;
|
|
304
|
+
out.push(s);
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const byId = new Map(selection.map((c) => [c.id, c]));
|
|
310
|
+
const target = out || '-';
|
|
311
|
+
/** @type {{ id: string }[]} */
|
|
312
|
+
let fresh = [];
|
|
313
|
+
let kind = 'new';
|
|
314
|
+
|
|
315
|
+
if (unused.length > 0) {
|
|
316
|
+
fresh = unused.slice(0, planned.size);
|
|
317
|
+
} else {
|
|
318
|
+
fresh = open
|
|
319
|
+
.slice(0, planned.size)
|
|
320
|
+
.map((e) => byId.get(e.id))
|
|
321
|
+
.filter((c) => c !== undefined);
|
|
322
|
+
kind = 'difficult';
|
|
323
|
+
}
|
|
324
|
+
if (fresh.length === 0) {
|
|
325
|
+
die(
|
|
326
|
+
'nothing left: no unused cases and no open difficult ones' +
|
|
327
|
+
(stuck.length ? ` (${stuck.length} stuck)` : '') +
|
|
328
|
+
' - run the final check',
|
|
329
|
+
1,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const inBatch = new Set(fresh.map((c) => c.id));
|
|
334
|
+
const pool = [...used].filter((id) => !inBatch.has(id) && byId.has(id));
|
|
335
|
+
const drawKeys = keys(((SEED * 7919 + number * 104729) % 2147483646) + 1, pool.length);
|
|
336
|
+
const order = new Map(pool.map((id, i) => [id, drawKeys[i]]));
|
|
337
|
+
const first = [...open, ...fixed].map((e) => e.id).filter((id) => order.has(id));
|
|
338
|
+
const rest = pool
|
|
339
|
+
.filter((id) => !first.includes(id))
|
|
340
|
+
.sort(
|
|
341
|
+
(a, b) =>
|
|
342
|
+
Number(complexity.get(b) === 'complex') - Number(complexity.get(a) === 'complex') ||
|
|
343
|
+
/** @type {number} */ (order.get(a)) - /** @type {number} */ (order.get(b)),
|
|
344
|
+
);
|
|
345
|
+
const rechecked = [...first, ...rest]
|
|
346
|
+
.slice(0, RECHECK)
|
|
347
|
+
.map((id) => /** @type {{ id: string }} */ (byId.get(id)));
|
|
348
|
+
|
|
349
|
+
const body = `${JSON.stringify({ batch: [...fresh, ...rechecked] }, null, 2)}\n`;
|
|
350
|
+
if (out) {
|
|
351
|
+
mkdirSync(dirname(resolve(out)), { recursive: true });
|
|
352
|
+
writeFileSync(out, body);
|
|
353
|
+
// Beside cases.json, never inside it: `zen run batch` allows nothing else there.
|
|
354
|
+
const plan = {
|
|
355
|
+
batch: number,
|
|
356
|
+
size: planned.size,
|
|
357
|
+
why: planned.why,
|
|
358
|
+
kind,
|
|
359
|
+
new: fresh.map((c) => c.id),
|
|
360
|
+
recheck: rechecked.map((c) => c.id),
|
|
361
|
+
};
|
|
362
|
+
writeFileSync(join(dirname(out), 'plan.json'), `${JSON.stringify(plan, null, 2)}\n`);
|
|
363
|
+
} else {
|
|
364
|
+
process.stdout.write(body);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// On stderr so it survives a redirect of stdout.
|
|
368
|
+
const left = kind === 'new' ? unused.length - fresh.length : 0;
|
|
369
|
+
console.error('');
|
|
370
|
+
console.error(`${NAME}: batch ${number} -> ${target}`);
|
|
371
|
+
console.error(` size ${planned.size} - ${planned.why}`);
|
|
372
|
+
console.error(
|
|
373
|
+
` ${kind === 'new' ? 'new ' : 'difficult'} ${fresh.map((c) => c.id).join(' ')}`,
|
|
374
|
+
);
|
|
375
|
+
console.error(
|
|
376
|
+
` recheck ${rechecked.length ? rechecked.map((c) => c.id).join(' ') : '(none - nothing used yet)'}`,
|
|
377
|
+
);
|
|
378
|
+
console.error(
|
|
379
|
+
` after it: ${left} unused case(s), ${open.length} difficult open` +
|
|
380
|
+
(existsSync(DIFFICULT) ? '' : ' (no difficult.json yet)'),
|
|
381
|
+
);
|
|
382
|
+
if (out && !RUN_NAME.test(dirname(out).split(/[\\/]/).pop() ?? '')) {
|
|
383
|
+
console.error(
|
|
384
|
+
` warning: ${dirname(out)} is not named batch<NN>-nomem-run1 - next.mjs will not see it`,
|
|
385
|
+
);
|
|
386
|
+
} else if (out && dirname(out).split(/[\\/]/).pop() !== `batch${pad(number)}-nomem-run1`) {
|
|
387
|
+
console.error(
|
|
388
|
+
` warning: this is batch ${number}; write it to batch${pad(number)}-nomem-run1/`,
|
|
389
|
+
);
|
|
390
|
+
}
|