@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.
Files changed (199) hide show
  1. package/README.md +65 -0
  2. package/dist/ask.js +28 -5
  3. package/dist/banner.js +2 -2
  4. package/dist/batch.d.ts +6 -0
  5. package/dist/batch.js +76 -1
  6. package/dist/catalog.d.ts +31 -1
  7. package/dist/catalog.js +140 -7
  8. package/dist/commands/index.js +2 -0
  9. package/dist/commands/inspect.d.ts +52 -2
  10. package/dist/commands/inspect.js +220 -31
  11. package/dist/commands/memory.js +156 -32
  12. package/dist/commands/meta.js +333 -24
  13. package/dist/commands/models.js +97 -12
  14. package/dist/commands/open.js +4 -4
  15. package/dist/commands/run.js +38 -4
  16. package/dist/commands/sandbox.js +148 -16
  17. package/dist/commands/view.d.ts +8 -0
  18. package/dist/commands/view.js +114 -0
  19. package/dist/engine.d.ts +8 -1
  20. package/dist/engine.js +7 -0
  21. package/dist/eventlog.d.ts +64 -0
  22. package/dist/eventlog.js +170 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +35 -3
  25. package/dist/main.js +0 -0
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +43 -0
  28. package/dist/meta/finetune/analyze-brief.d.ts +26 -0
  29. package/dist/meta/finetune/analyze-brief.js +131 -0
  30. package/dist/meta/finetune/apply-brief.d.ts +23 -0
  31. package/dist/meta/finetune/apply-brief.js +247 -0
  32. package/dist/meta/finetune/brief.d.ts +45 -0
  33. package/dist/meta/finetune/brief.js +158 -0
  34. package/dist/meta/finetune/children.d.ts +8 -0
  35. package/dist/meta/finetune/children.js +76 -0
  36. package/dist/meta/finetune/command.d.ts +30 -0
  37. package/dist/meta/finetune/command.js +516 -0
  38. package/dist/meta/finetune/cost.d.ts +46 -0
  39. package/dist/meta/finetune/cost.js +105 -0
  40. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  41. package/dist/meta/finetune/dataset/anchors.js +239 -0
  42. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  43. package/dist/meta/finetune/dataset/changes.js +319 -0
  44. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  45. package/dist/meta/finetune/dataset/command.js +1011 -0
  46. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/drift.js +71 -0
  48. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  49. package/dist/meta/finetune/dataset/export.js +51 -0
  50. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  51. package/dist/meta/finetune/dataset/index.js +2 -0
  52. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  53. package/dist/meta/finetune/dataset/sample.js +197 -0
  54. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  55. package/dist/meta/finetune/dataset/schema.js +216 -0
  56. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  57. package/dist/meta/finetune/dataset/store.js +177 -0
  58. package/dist/meta/finetune/dataset/types.d.ts +127 -0
  59. package/dist/meta/finetune/dataset/types.js +39 -0
  60. package/dist/meta/finetune/failure.d.ts +15 -0
  61. package/dist/meta/finetune/failure.js +26 -0
  62. package/dist/meta/finetune/feedback.d.ts +57 -0
  63. package/dist/meta/finetune/feedback.js +104 -0
  64. package/dist/meta/finetune/guard.d.ts +29 -0
  65. package/dist/meta/finetune/guard.js +104 -0
  66. package/dist/meta/finetune/improvements.d.ts +98 -0
  67. package/dist/meta/finetune/improvements.js +501 -0
  68. package/dist/meta/finetune/index.d.ts +2 -0
  69. package/dist/meta/finetune/index.js +2 -0
  70. package/dist/meta/finetune/journal.d.ts +35 -0
  71. package/dist/meta/finetune/journal.js +135 -0
  72. package/dist/meta/finetune/live.d.ts +84 -0
  73. package/dist/meta/finetune/live.js +272 -0
  74. package/dist/meta/finetune/loop.d.ts +3 -0
  75. package/dist/meta/finetune/loop.js +163 -0
  76. package/dist/meta/finetune/memories.d.ts +47 -0
  77. package/dist/meta/finetune/memories.js +154 -0
  78. package/dist/meta/finetune/recheck.d.ts +58 -0
  79. package/dist/meta/finetune/recheck.js +311 -0
  80. package/dist/meta/finetune/report.d.ts +40 -0
  81. package/dist/meta/finetune/report.js +1397 -0
  82. package/dist/meta/finetune/retry.d.ts +24 -0
  83. package/dist/meta/finetune/retry.js +96 -0
  84. package/dist/meta/finetune/sampler.d.ts +18 -0
  85. package/dist/meta/finetune/sampler.js +47 -0
  86. package/dist/meta/finetune/system.d.ts +27 -0
  87. package/dist/meta/finetune/system.js +132 -0
  88. package/dist/meta/finetune/train.d.ts +43 -0
  89. package/dist/meta/finetune/train.js +425 -0
  90. package/dist/meta/finetune/tui.d.ts +16 -0
  91. package/dist/meta/finetune/tui.js +316 -0
  92. package/dist/meta/finetune/tuning.d.ts +193 -0
  93. package/dist/meta/finetune/tuning.js +308 -0
  94. package/dist/meta/finetune/usage.d.ts +66 -0
  95. package/dist/meta/finetune/usage.js +260 -0
  96. package/dist/meta/finetune/view.d.ts +9 -0
  97. package/dist/meta/finetune/view.js +27 -0
  98. package/dist/meta/host.d.ts +15 -0
  99. package/dist/meta/host.js +21 -0
  100. package/dist/meta/index.d.ts +11 -0
  101. package/dist/meta/index.js +11 -0
  102. package/dist/meta/launch.d.ts +81 -0
  103. package/dist/meta/launch.js +286 -0
  104. package/dist/meta/model.d.ts +44 -0
  105. package/dist/meta/model.js +119 -0
  106. package/dist/meta/prompts.d.ts +18 -0
  107. package/dist/meta/prompts.js +61 -0
  108. package/dist/meta/relay.d.ts +20 -0
  109. package/dist/meta/relay.js +123 -0
  110. package/dist/meta/tokens.d.ts +52 -0
  111. package/dist/meta/tokens.js +226 -0
  112. package/dist/meta/window.d.ts +61 -0
  113. package/dist/meta/window.js +198 -0
  114. package/dist/meta/wire.d.ts +42 -0
  115. package/dist/meta/wire.js +129 -0
  116. package/dist/meta.d.ts +71 -1
  117. package/dist/meta.js +303 -6
  118. package/dist/modelref.d.ts +16 -0
  119. package/dist/modelref.js +41 -0
  120. package/dist/narrate.js +5 -2
  121. package/dist/podman.d.ts +26 -0
  122. package/dist/podman.js +83 -3
  123. package/dist/projects.d.ts +0 -5
  124. package/dist/projects.js +2 -15
  125. package/dist/resources.d.ts +215 -0
  126. package/dist/resources.js +856 -0
  127. package/dist/sandbox.d.ts +2 -0
  128. package/dist/sandbox.js +10 -3
  129. package/dist/scaffold.d.ts +15 -7
  130. package/dist/scaffold.js +24 -67
  131. package/dist/session.js +4 -29
  132. package/dist/term.d.ts +2 -0
  133. package/dist/term.js +11 -2
  134. package/dist/trace.d.ts +5 -0
  135. package/dist/trace.js +107 -7
  136. package/dist/tui/app.js +19 -5
  137. package/dist/tui/links.d.ts +27 -0
  138. package/dist/tui/links.js +142 -0
  139. package/dist/tui/models.d.ts +35 -0
  140. package/dist/tui/models.js +447 -0
  141. package/dist/tui/sandbox.d.ts +10 -0
  142. package/dist/tui/sandbox.js +131 -0
  143. package/dist/tui/wrap.d.ts +5 -0
  144. package/dist/tui/wrap.js +12 -0
  145. package/dist/usage.d.ts +78 -0
  146. package/dist/usage.js +51 -0
  147. package/package.json +4 -2
  148. package/templates/editor/.github/copilot-instructions.md +60 -145
  149. package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
  150. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  151. package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
  152. package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
  153. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  154. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  155. package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
  156. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
  157. package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
  158. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  159. package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
  160. package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
  161. package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
  162. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  163. package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
  164. package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
  165. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  166. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
  167. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  168. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  169. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  170. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
  171. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  172. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  173. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  174. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  175. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  176. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  177. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  178. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  179. package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
  180. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
  181. package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
  182. package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
  183. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  184. package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
  185. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
  186. package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
  187. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
  188. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
  189. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
  190. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  191. package/templates/editor/.vscode/settings.json +1 -0
  192. package/templates/project/agents/files-instructions.md +29 -20
  193. package/templates/project/agents/fork-instructions.md +78 -28
  194. package/templates/project/agents/memory-instructions.md +222 -494
  195. package/templates/project/agents/tools-instructions.md +14 -4
  196. package/templates/project/gitignore +2 -0
  197. package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
  198. package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
  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
+ );