@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,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
//
|
|
4
|
+
// Keep the list of difficult cases: the ones that caused the most trouble.
|
|
5
|
+
//
|
|
6
|
+
// A case goes on the list when grading shows it is hard for the project - it
|
|
7
|
+
// stayed wrong after a fix, it broke as a recheck case, it flipped between right
|
|
8
|
+
// and wrong on the same prose, memory made it wrong or no cheaper, or it costs
|
|
9
|
+
// far more than its batch. batch.mjs
|
|
10
|
+
// puts open cases first in every later batch's rechecks, and fills whole batches
|
|
11
|
+
// with them once the selection is used up, until they are fixed; fixed ones stay
|
|
12
|
+
// ahead of the other rechecks, because what was hard once breaks again first.
|
|
13
|
+
//
|
|
14
|
+
// A case is `stuck` once `maxRetries` batches (config.json, default 3) have held
|
|
15
|
+
// it since it was added, without it being fixed: that is the model's ceiling or a
|
|
16
|
+
// rubric problem, and it is reported rather than retried forever.
|
|
17
|
+
//
|
|
18
|
+
// Run it from anywhere; it finds the project root from its own location.
|
|
19
|
+
// See .github/skills/zen-finetune/SKILL.md, "Difficult cases".
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { dirname, join, resolve } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const NAME = 'difficult.mjs';
|
|
26
|
+
|
|
27
|
+
const REASONS = ['wrong', 'regressed', 'flaky', 'memory', 'costly'];
|
|
28
|
+
|
|
29
|
+
const USAGE = `Keep the list of difficult cases: the ones that caused the most trouble.
|
|
30
|
+
|
|
31
|
+
${NAME} the list: status, reasons, retries
|
|
32
|
+
${NAME} add <id...> --why <reason> --run <run> [--note <text>]
|
|
33
|
+
${NAME} fix <id...> --run <run>
|
|
34
|
+
|
|
35
|
+
reasons wrong still wrong after a run that tried to fix it
|
|
36
|
+
regressed a recheck case that failed
|
|
37
|
+
flaky right in one run, wrong in a later run of the same cases
|
|
38
|
+
memory with memory: wrong, no cheaper, or re-committing what it
|
|
39
|
+
recalled - still, after a run that tried to fix it
|
|
40
|
+
costly over 2x the batch's median llm calls in its passing run
|
|
41
|
+
|
|
42
|
+
status open comes first in every later batch until fixed
|
|
43
|
+
fixed right in a PASSED run; still preferred as a recheck case
|
|
44
|
+
stuck open after maxRetries batches; reported, not retried
|
|
45
|
+
|
|
46
|
+
<run> is a run directory name, e.g. batch03-nomem-run2 or batch03-mem-run1`;
|
|
47
|
+
|
|
48
|
+
process.stdout.on('error', (err) => {
|
|
49
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'EPIPE') {
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
process.chdir(resolve(dirname(fileURLToPath(import.meta.url)), '../../../..'));
|
|
55
|
+
|
|
56
|
+
const FILE = 'finetune/difficult.json';
|
|
57
|
+
const CONFIG = 'finetune/config.json';
|
|
58
|
+
const RUNS = 'finetune/runs';
|
|
59
|
+
const RUN_NAME = /^batch(\d{2})-(nomem|mem)-run(\d+)$/;
|
|
60
|
+
|
|
61
|
+
/** @param {string} msg @param {number} [code] @returns {never} */
|
|
62
|
+
function die(msg, code = 2) {
|
|
63
|
+
console.error(`${NAME}: ${msg}`);
|
|
64
|
+
process.exit(code);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string} path @returns {any} */
|
|
68
|
+
function json(path) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {{ id: string, reasons: string[], added: string, notes: string[],
|
|
78
|
+
* fixed?: string }} Entry
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/** @type {{ cases: Entry[] }} */
|
|
82
|
+
const doc = json(FILE) ?? { cases: [] };
|
|
83
|
+
const maxRetries = Number(json(CONFIG)?.maxRetries) || 3;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* How many batches have held it since it was added, read off the first
|
|
87
|
+
* no-memory run of every batch.
|
|
88
|
+
*
|
|
89
|
+
* @param {Entry} e
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
function retries(e) {
|
|
93
|
+
const since = Number(RUN_NAME.exec(e.added)?.[1] ?? 0);
|
|
94
|
+
/** @type {string[]} */
|
|
95
|
+
let names = [];
|
|
96
|
+
try {
|
|
97
|
+
names = readdirSync(RUNS);
|
|
98
|
+
} catch {
|
|
99
|
+
names = [];
|
|
100
|
+
}
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
const m = RUN_NAME.exec(name);
|
|
104
|
+
if (!m || m[2] !== 'nomem' || +m[3] !== 1 || +m[1] <= since) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const ids = (json(join(RUNS, name, 'cases.json'))?.batch ?? []).map(
|
|
108
|
+
(/** @type {{ id: string }} */ c) => c.id,
|
|
109
|
+
);
|
|
110
|
+
if (ids.includes(e.id)) {
|
|
111
|
+
count++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return count;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @param {Entry} e @returns {'open' | 'fixed' | 'stuck'} */
|
|
118
|
+
function status(e) {
|
|
119
|
+
if (e.fixed) {
|
|
120
|
+
return 'fixed';
|
|
121
|
+
}
|
|
122
|
+
return retries(e) >= maxRetries ? 'stuck' : 'open';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function save() {
|
|
126
|
+
const tmp = `${FILE}.tmp`;
|
|
127
|
+
writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}\n`);
|
|
128
|
+
renameSync(tmp, FILE);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
const argv = process.argv.slice(2);
|
|
134
|
+
const command = argv[0] && !argv[0].startsWith('-') ? /** @type {string} */ (argv.shift()) : 'list';
|
|
135
|
+
|
|
136
|
+
/** @type {string[]} */
|
|
137
|
+
const ids = [];
|
|
138
|
+
let why = '';
|
|
139
|
+
let run = '';
|
|
140
|
+
let note = '';
|
|
141
|
+
while (argv.length > 0) {
|
|
142
|
+
const arg = /** @type {string} */ (argv.shift());
|
|
143
|
+
const value = () => argv.shift() ?? die(`${arg} needs a value`);
|
|
144
|
+
if (arg === '--why') {
|
|
145
|
+
why = value();
|
|
146
|
+
} else if (arg === '--run') {
|
|
147
|
+
run = value();
|
|
148
|
+
} else if (arg === '--note') {
|
|
149
|
+
note = value();
|
|
150
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
151
|
+
console.log(USAGE);
|
|
152
|
+
process.exit(0);
|
|
153
|
+
} else if (arg.startsWith('-')) {
|
|
154
|
+
die(`unknown flag ${arg}`);
|
|
155
|
+
} else {
|
|
156
|
+
ids.push(arg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (command === 'add' || command === 'fix') {
|
|
161
|
+
if (ids.length === 0) {
|
|
162
|
+
die(`${command} needs at least one case id`);
|
|
163
|
+
}
|
|
164
|
+
if (!RUN_NAME.test(run)) {
|
|
165
|
+
die('--run must name a run, e.g. batch03-nomem-run2');
|
|
166
|
+
}
|
|
167
|
+
if (!existsSync(join(RUNS, run))) {
|
|
168
|
+
die(`no ${RUNS}/${run}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (command === 'add') {
|
|
172
|
+
if (!REASONS.includes(why)) {
|
|
173
|
+
die(`--why must be one of: ${REASONS.join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
for (const id of ids) {
|
|
176
|
+
const held = doc.cases.find((e) => e.id === id);
|
|
177
|
+
if (held) {
|
|
178
|
+
held.reasons = [...new Set([...held.reasons, why])];
|
|
179
|
+
// Back on the list after a fix: it was not fixed, and its retries start again.
|
|
180
|
+
if (held.fixed) {
|
|
181
|
+
delete held.fixed;
|
|
182
|
+
held.added = run;
|
|
183
|
+
}
|
|
184
|
+
if (note) {
|
|
185
|
+
held.notes.push(`${run}: ${note}`);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
doc.cases.push({
|
|
189
|
+
id,
|
|
190
|
+
reasons: [why],
|
|
191
|
+
added: run,
|
|
192
|
+
notes: note ? [`${run}: ${note}`] : [],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
for (const id of ids) {
|
|
198
|
+
const held = doc.cases.find((e) => e.id === id);
|
|
199
|
+
if (!held) {
|
|
200
|
+
die(`${id} is not on the list`);
|
|
201
|
+
}
|
|
202
|
+
held.fixed = run;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
save();
|
|
206
|
+
console.error(`${NAME}: ${command === 'add' ? 'added' : 'fixed'} ${ids.join(' ')}`);
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (command !== 'list') {
|
|
211
|
+
console.error(`${NAME}: unknown command ${command}`);
|
|
212
|
+
console.error(USAGE);
|
|
213
|
+
process.exit(2);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (doc.cases.length === 0) {
|
|
217
|
+
console.log('no difficult cases yet');
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const rows = doc.cases.map((e) => [
|
|
222
|
+
e.id,
|
|
223
|
+
status(e),
|
|
224
|
+
e.reasons.join(','),
|
|
225
|
+
String(retries(e)),
|
|
226
|
+
e.added,
|
|
227
|
+
e.fixed ?? '-',
|
|
228
|
+
]);
|
|
229
|
+
const header = ['CASE', 'STATUS', 'WHY', 'RETRIES', 'ADDED', 'FIXED'];
|
|
230
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
231
|
+
for (const row of [header, ...rows]) {
|
|
232
|
+
console.log(
|
|
233
|
+
row
|
|
234
|
+
.map((cell, i) => cell.padEnd(widths[i]))
|
|
235
|
+
.join(' ')
|
|
236
|
+
.trimEnd(),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const count = (/** @type {string} */ s) => doc.cases.filter((e) => status(e) === s).length;
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(
|
|
242
|
+
`${count('open')} open · ${count('fixed')} fixed · ${count('stuck')} stuck (max ${maxRetries} retries)`,
|
|
243
|
+
);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
//
|
|
4
|
+
// Keep the memory the with-memory runs read: the candidate for the batch being
|
|
5
|
+
// tuned, and the last good memory every passed batch has added to.
|
|
6
|
+
//
|
|
7
|
+
// A batch's no-memory runs each start from an empty graph, and every case writes
|
|
8
|
+
// its own. When one of them passes, `candidate <run>` copies the last good
|
|
9
|
+
// memory into a NEW directory and folds what each of that run's cases committed
|
|
10
|
+
// into it with `zen memory merge`, which drops what the graph already held. That
|
|
11
|
+
// is the candidate: the memory the batch's with-memory runs start from, every
|
|
12
|
+
// case from its own copy, so nothing a case commits reaches the candidate or
|
|
13
|
+
// another case.
|
|
14
|
+
//
|
|
15
|
+
// When a with-memory run passes, `checkpoint <run>` makes the candidate the last
|
|
16
|
+
// good memory: it renames the directory and only then repoints
|
|
17
|
+
// finetune/memory/last-good.json at it. `zen memory merge` writes its files one
|
|
18
|
+
// after another, so a merge killed half-way can leave a graph whose files
|
|
19
|
+
// disagree - but only ever a candidate, never the one the pointer names. Stop the
|
|
20
|
+
// tuning at any moment and `memory.mjs path` still names a usable graph.
|
|
21
|
+
//
|
|
22
|
+
// Needs `zen` on PATH. Run it from anywhere; it finds the project root from its
|
|
23
|
+
// own location. See .github/skills/zen-finetune/SKILL.md, "The memory".
|
|
24
|
+
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
import {
|
|
27
|
+
cpSync,
|
|
28
|
+
existsSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
renameSync,
|
|
33
|
+
rmSync,
|
|
34
|
+
writeFileSync,
|
|
35
|
+
} from 'node:fs';
|
|
36
|
+
import { dirname, join, resolve } from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
|
|
39
|
+
const NAME = 'memory.mjs';
|
|
40
|
+
|
|
41
|
+
const USAGE = `Keep the candidate memory of the batch being tuned, and the last good memory.
|
|
42
|
+
|
|
43
|
+
${NAME} the last good memory and the candidate, and what built them
|
|
44
|
+
${NAME} path the last good memory's directory
|
|
45
|
+
${NAME} path --candidate the candidate's directory, for --memory "$(...)"
|
|
46
|
+
${NAME} candidate <run> a PASSED no-memory run: last good + what its cases committed
|
|
47
|
+
${NAME} checkpoint <run> a PASSED with-memory run: its candidate becomes last good
|
|
48
|
+
|
|
49
|
+
<run> is a run directory name, e.g. batch03-nomem-run2 or batch03-mem-run1`;
|
|
50
|
+
|
|
51
|
+
process.chdir(resolve(dirname(fileURLToPath(import.meta.url)), '../../../..'));
|
|
52
|
+
|
|
53
|
+
const ROOT = 'finetune/memory';
|
|
54
|
+
const POINTER = `${ROOT}/last-good.json`;
|
|
55
|
+
const CANDIDATE = `${ROOT}/candidate.json`;
|
|
56
|
+
const RUNS = 'finetune/runs';
|
|
57
|
+
const RUN_NAME = /^batch(\d{2})-(nomem|mem)-run(\d+)$/;
|
|
58
|
+
// Older graphs are kept to roll back to; beyond this many they are only disk.
|
|
59
|
+
const KEEP = 3;
|
|
60
|
+
|
|
61
|
+
/** @param {string} msg @param {number} [code] @returns {never} */
|
|
62
|
+
function die(msg, code = 2) {
|
|
63
|
+
console.error(`${NAME}: ${msg}`);
|
|
64
|
+
process.exit(code);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string} path @returns {any} */
|
|
68
|
+
function json(path) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {string} path @param {unknown} value */
|
|
77
|
+
function writeAtomic(path, value) {
|
|
78
|
+
const tmp = `${path}.tmp`;
|
|
79
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
80
|
+
renameSync(tmp, path);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @param {string} dir @returns {number} */
|
|
84
|
+
const nodesIn = (dir) => Number(json(join(dir, 'manifest.json'))?.nodes) || 0;
|
|
85
|
+
|
|
86
|
+
/** @param {string} run */
|
|
87
|
+
function passed(run) {
|
|
88
|
+
const path = join(RUNS, run, 'findings.md');
|
|
89
|
+
return existsSync(path) && /^## PASSED/m.test(readFileSync(path, 'utf8'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {{ dir: string, batches: number[], runs: string[], nodes: number, at: string }} Pointer
|
|
94
|
+
* @typedef {{ dir: string, batch: number, from: string, nodes: number, at: string }} Candidate
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/** @type {Pointer | undefined} */
|
|
98
|
+
const pointer = json(POINTER);
|
|
99
|
+
/** @type {Candidate | undefined} */
|
|
100
|
+
const candidate = json(CANDIDATE);
|
|
101
|
+
|
|
102
|
+
const [command = 'status', arg] = process.argv.slice(2);
|
|
103
|
+
|
|
104
|
+
if (command === '-h' || command === '--help') {
|
|
105
|
+
console.log(USAGE);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (command === 'path') {
|
|
110
|
+
if (arg === '--candidate') {
|
|
111
|
+
if (!candidate) {
|
|
112
|
+
die(
|
|
113
|
+
'no candidate - build one from a PASSED no-memory run: memory.mjs candidate <run>',
|
|
114
|
+
1,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
console.log(candidate.dir);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
if (arg !== undefined) {
|
|
121
|
+
die(`unknown argument ${arg}`);
|
|
122
|
+
}
|
|
123
|
+
if (!pointer) {
|
|
124
|
+
die('no last good memory yet - checkpoint a PASSED with-memory run first', 1);
|
|
125
|
+
}
|
|
126
|
+
console.log(pointer.dir);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (command === 'status') {
|
|
131
|
+
if (pointer) {
|
|
132
|
+
console.log(`last good ${pointer.dir}`);
|
|
133
|
+
console.log(`nodes ${pointer.nodes}`);
|
|
134
|
+
console.log(`updated ${pointer.at}`);
|
|
135
|
+
console.log(`batches ${pointer.batches.join(', ')}`);
|
|
136
|
+
console.log(`built from ${pointer.runs.join(' ')}`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log('no last good memory yet');
|
|
139
|
+
}
|
|
140
|
+
console.log('');
|
|
141
|
+
if (candidate) {
|
|
142
|
+
console.log(`candidate ${candidate.dir}`);
|
|
143
|
+
console.log(`batch ${candidate.batch}, from ${candidate.from}`);
|
|
144
|
+
console.log(`nodes ${candidate.nodes}`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log('no candidate');
|
|
147
|
+
}
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (command !== 'candidate' && command !== 'checkpoint') {
|
|
152
|
+
console.error(`${NAME}: unknown command ${command}`);
|
|
153
|
+
console.error(USAGE);
|
|
154
|
+
process.exit(2);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const m = RUN_NAME.exec(arg ?? '');
|
|
158
|
+
if (!arg || !m) {
|
|
159
|
+
die(`${command} needs a run, e.g. batch03-${command === 'candidate' ? 'nomem' : 'mem'}-run2`);
|
|
160
|
+
}
|
|
161
|
+
const batch = +m[1];
|
|
162
|
+
const pad = String(batch).padStart(2, '0');
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// candidate
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
if (command === 'candidate') {
|
|
169
|
+
if (m[2] !== 'nomem') {
|
|
170
|
+
die(
|
|
171
|
+
'a candidate is built from a no-memory run: a with-memory run holds the whole graph again in every case',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (!passed(arg)) {
|
|
175
|
+
die(`${arg} does not say PASSED - only a passing run's memory is kept`);
|
|
176
|
+
}
|
|
177
|
+
if (pointer?.batches.includes(batch)) {
|
|
178
|
+
die(`batch ${batch} is already in the last good memory`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Only case memories with a manifest: a case that committed nothing has none.
|
|
182
|
+
const dir = join(RUNS, arg, 'batch');
|
|
183
|
+
const sources = (existsSync(dir) ? readdirSync(dir, { withFileTypes: true }) : [])
|
|
184
|
+
.filter((e) => e.isDirectory() && existsSync(join(dir, e.name, 'memory', 'manifest.json')))
|
|
185
|
+
.map((e) => join(dir, e.name, 'memory'));
|
|
186
|
+
|
|
187
|
+
const target = join(ROOT, `candidate-batch${pad}`);
|
|
188
|
+
rmSync(target, { recursive: true, force: true });
|
|
189
|
+
mkdirSync(ROOT, { recursive: true });
|
|
190
|
+
if (pointer && existsSync(pointer.dir)) {
|
|
191
|
+
cpSync(pointer.dir, target, { recursive: true, filter: (src) => !src.endsWith('/.lock') });
|
|
192
|
+
} else {
|
|
193
|
+
mkdirSync(target, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sources.length > 0) {
|
|
197
|
+
const merged = spawnSync('zen', ['memory', 'merge', ...sources, '--dir', target, '--yes'], {
|
|
198
|
+
stdio: 'inherit',
|
|
199
|
+
});
|
|
200
|
+
if (merged.error) {
|
|
201
|
+
rmSync(target, { recursive: true, force: true });
|
|
202
|
+
die(`could not run zen: ${merged.error.message}`);
|
|
203
|
+
}
|
|
204
|
+
if (merged.status !== 0) {
|
|
205
|
+
rmSync(target, { recursive: true, force: true });
|
|
206
|
+
die('zen memory merge failed - no candidate was written', 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** @type {Candidate} */
|
|
211
|
+
const next = {
|
|
212
|
+
dir: target,
|
|
213
|
+
batch,
|
|
214
|
+
from: arg,
|
|
215
|
+
nodes: nodesIn(target),
|
|
216
|
+
at: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
writeAtomic(CANDIDATE, next);
|
|
219
|
+
const before = pointer ? nodesIn(pointer.dir) : 0;
|
|
220
|
+
console.error(
|
|
221
|
+
`${NAME}: candidate for batch ${batch} is ${target} - ${next.nodes} nodes ` +
|
|
222
|
+
`(${before} from the last good memory, ${sources.length} case memor${sources.length === 1 ? 'y' : 'ies'} from ${arg})`,
|
|
223
|
+
);
|
|
224
|
+
if (next.nodes === 0) {
|
|
225
|
+
console.error(
|
|
226
|
+
`${NAME}: the candidate is EMPTY - nothing was ever committed, so a with-memory run ` +
|
|
227
|
+
'measures nothing. That is a commit-policy finding for the no-memory runs.',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// checkpoint
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
if (m[2] !== 'mem') {
|
|
238
|
+
die('checkpoint takes a PASSED with-memory run; a no-memory run builds a candidate first');
|
|
239
|
+
}
|
|
240
|
+
if (!passed(arg)) {
|
|
241
|
+
die(
|
|
242
|
+
`${arg} does not say PASSED - the candidate is kept only once its batch passes with memory`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (pointer?.batches.includes(batch)) {
|
|
246
|
+
console.error(`${NAME}: batch ${batch} is already in the last good memory`);
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
if (!candidate || candidate.batch !== batch) {
|
|
250
|
+
die(`no candidate for batch ${batch} - memory.mjs candidate batch${pad}-nomem-run<N>`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const target = join(ROOT, `after-batch${pad}`);
|
|
254
|
+
if (existsSync(candidate.dir)) {
|
|
255
|
+
rmSync(target, { recursive: true, force: true });
|
|
256
|
+
renameSync(candidate.dir, target);
|
|
257
|
+
} else if (!existsSync(target)) {
|
|
258
|
+
die(`the candidate ${candidate.dir} is gone - build it again from ${candidate.from}`);
|
|
259
|
+
}
|
|
260
|
+
// A checkpoint killed after the rename resumes here: the directory is already in place.
|
|
261
|
+
|
|
262
|
+
/** @type {Pointer} */
|
|
263
|
+
const next = {
|
|
264
|
+
dir: target,
|
|
265
|
+
batches: [...(pointer?.batches ?? []), batch],
|
|
266
|
+
runs: [...(pointer?.runs ?? []), candidate.from, arg],
|
|
267
|
+
nodes: nodesIn(target),
|
|
268
|
+
at: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
writeAtomic(POINTER, next);
|
|
271
|
+
rmSync(CANDIDATE, { force: true });
|
|
272
|
+
|
|
273
|
+
const history = next.batches.map((b) => join(ROOT, `after-batch${String(b).padStart(2, '0')}`));
|
|
274
|
+
for (const dir of history.slice(0, -KEEP)) {
|
|
275
|
+
rmSync(dir, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.error(
|
|
279
|
+
`${NAME}: last good memory is now ${target} - ${next.nodes} nodes, batches ${next.batches.join(', ')}`,
|
|
280
|
+
);
|