@zenera/cli 1.1.25 → 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 (96) hide show
  1. package/README.md +13 -1
  2. package/dist/catalog.d.ts +20 -0
  3. package/dist/catalog.js +60 -0
  4. package/dist/commands/index.js +2 -0
  5. package/dist/commands/inspect.d.ts +5 -1
  6. package/dist/commands/inspect.js +54 -14
  7. package/dist/commands/memory.js +28 -3
  8. package/dist/commands/meta.js +24 -5
  9. package/dist/commands/models.js +2 -1
  10. package/dist/commands/sandbox.js +73 -3
  11. package/dist/commands/view.d.ts +8 -0
  12. package/dist/commands/view.js +114 -0
  13. package/dist/engine.d.ts +3 -1
  14. package/dist/engine.js +6 -0
  15. package/dist/eventlog.d.ts +5 -0
  16. package/dist/eventlog.js +25 -1
  17. package/dist/liveness.js +20 -2
  18. package/dist/main.js +0 -0
  19. package/dist/meta/editor.d.ts +1 -1
  20. package/dist/meta/editor.js +4 -21
  21. package/dist/meta/finetune/analyze-brief.d.ts +26 -0
  22. package/dist/meta/finetune/analyze-brief.js +131 -0
  23. package/dist/meta/finetune/apply-brief.d.ts +23 -0
  24. package/dist/meta/finetune/apply-brief.js +247 -0
  25. package/dist/meta/finetune/brief.d.ts +45 -0
  26. package/dist/meta/finetune/brief.js +158 -0
  27. package/dist/meta/finetune/command.d.ts +1 -1
  28. package/dist/meta/finetune/command.js +224 -46
  29. package/dist/meta/finetune/cost.d.ts +46 -0
  30. package/dist/meta/finetune/cost.js +105 -0
  31. package/dist/meta/finetune/dataset/types.d.ts +3 -1
  32. package/dist/meta/finetune/feedback.d.ts +14 -2
  33. package/dist/meta/finetune/feedback.js +22 -3
  34. package/dist/meta/finetune/guard.d.ts +29 -0
  35. package/dist/meta/finetune/guard.js +104 -0
  36. package/dist/meta/finetune/improvements.d.ts +50 -3
  37. package/dist/meta/finetune/improvements.js +290 -33
  38. package/dist/meta/finetune/live.d.ts +17 -7
  39. package/dist/meta/finetune/live.js +56 -10
  40. package/dist/meta/finetune/loop.js +59 -10
  41. package/dist/meta/finetune/recheck.d.ts +58 -0
  42. package/dist/meta/finetune/recheck.js +311 -0
  43. package/dist/meta/finetune/report.d.ts +9 -2
  44. package/dist/meta/finetune/report.js +551 -438
  45. package/dist/meta/finetune/sampler.js +8 -2
  46. package/dist/meta/finetune/train.d.ts +6 -2
  47. package/dist/meta/finetune/train.js +55 -21
  48. package/dist/meta/finetune/tui.d.ts +3 -3
  49. package/dist/meta/finetune/tui.js +37 -12
  50. package/dist/meta/finetune/tuning.d.ts +27 -1
  51. package/dist/meta/finetune/tuning.js +21 -2
  52. package/dist/meta/finetune/usage.d.ts +4 -0
  53. package/dist/meta/finetune/usage.js +35 -0
  54. package/dist/meta/finetune/view.d.ts +9 -0
  55. package/dist/meta/finetune/view.js +27 -0
  56. package/dist/meta/host.d.ts +2 -1
  57. package/dist/meta/host.js +2 -1
  58. package/dist/meta.d.ts +291 -0
  59. package/dist/meta.js +981 -0
  60. package/dist/resources.d.ts +215 -0
  61. package/dist/resources.js +856 -0
  62. package/dist/scaffold.d.ts +3 -4
  63. package/dist/scaffold.js +21 -9
  64. package/dist/trace.d.ts +5 -0
  65. package/dist/trace.js +54 -5
  66. package/dist/tui/app.js +19 -5
  67. package/dist/tui/links.d.ts +27 -0
  68. package/dist/tui/links.js +142 -0
  69. package/dist/tui/models.d.ts +3 -1
  70. package/dist/tui/models.js +65 -7
  71. package/dist/tui/sandbox.d.ts +10 -0
  72. package/dist/tui/sandbox.js +131 -0
  73. package/package.json +3 -2
  74. package/templates/editor/.github/copilot-instructions.md +29 -43
  75. package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
  76. package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
  77. package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
  78. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
  79. package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
  80. package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
  81. package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
  82. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
  83. package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
  84. package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
  85. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
  86. package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
  87. package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
  88. package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
  89. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
  90. package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
  91. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
  92. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
  93. package/templates/project/agents/fork-instructions.md +78 -28
  94. package/templates/project/agents/memory-instructions.md +222 -493
  95. package/templates/project/agents/tools-instructions.md +7 -0
  96. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
@@ -1,30 +1,37 @@
1
1
  import { liveHolder } from '@zenera/neo';
2
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
4
5
  import { editorFiles } from "../editor.js";
5
- import { bold, cyan, dim, green, invalidError, json, note, parse, red, usageError, write, yellow, } from "../host.js";
6
+ import { bold, CliError, cyan, dim, EXIT, green, invalidError, json, note, parse, red, usageError, write, yellow, } from "../host.js";
7
+ import { costChanges, measure, overLimits } from "./cost.js";
6
8
  import { drift } from "./dataset/drift.js";
7
9
  import { DatasetStore } from "./dataset/store.js";
8
10
  import { runLoop } from "./loop.js";
9
- import { attemptsOf } from "./report.js";
11
+ import { analyzeSession, attemptsOf } from "./report.js";
10
12
  import { retry } from "./retry.js";
11
13
  import { resultOf, selection } from "./sampler.js";
12
- import { DEFAULTS, FAILURES_IN_A_ROW, FINETUNE_DIR, LEGACY_FINETUNE_DIR, spawnZen, Tuning, } from "./tuning.js";
14
+ import { DEFAULTS, FAILURES_IN_A_ROW, FINETUNE_DIR, LEGACY_FINETUNE_DIR, readJson, spawnZen, Tuning, } from "./tuning.js";
15
+ import { liveView, startView } from "./view.js";
13
16
  // ---------------------------------------------------------------------------
14
17
  // zen meta finetune
15
18
  // ---------------------------------------------------------------------------
16
- export const FINETUNE_USAGE = 'zen meta finetune <start|status|stop|retry|session> [options]';
19
+ export const FINETUNE_USAGE = 'zen meta finetune <start|status|stop|serve|retry|session|gate> [options]';
17
20
  export const FINETUNE_HELP = [
18
21
  'Tuning:',
19
22
  ' zen meta finetune start [options] train every case: run, analyze, apply, run again',
20
23
  ' zen meta finetune [status] where it stands; the detail is finetune/STATUS.md',
21
24
  ' zen meta finetune stop finish the steps in flight, then stop',
25
+ ' zen meta finetune serve the tuning live in a browser, while it is not running',
22
26
  ' zen meta finetune retry --difficult give difficult cases another round, then start',
23
- " zen meta finetune session <case> resume that case's analyze session",
27
+ " zen meta finetune session <case> resume the analyze session of that case's latest try",
28
+ ' zen meta finetune gate <apply dir> would this apply pass? - for the apply agent',
24
29
  '',
25
- ' start: -N <workers> (4), -M <apply at> (= workers), --tries <n> (4), --mem-tries <n> (3),',
26
- ' --merge-every <k> (3), --seed <n>, --class <c>, --id <glob>, --rubric yes|no, --limit <n>,',
27
- ' --force (past drift).',
30
+ ' start: -N <workers> (4), -M <apply at> (4), --tries <n> (4), --mem-tries <n> (3),',
31
+ ' --class <c>, --id <glob>, --rubric yes|no, --limit <n> and --more <n> (this start only);',
32
+ ' what an apply may do: --budget <bytes> (1500), --max-growth <pct> (50),',
33
+ ' --skill-cap <bytes> (12000), --max-skills <n> (10); after it, --recheck <n> (3).',
34
+ ' More - --merge-every, --seed, --force: zen meta finetune --help.',
28
35
  ' In a terminal start draws what runs now, the tokens and the log; --plain does not.',
29
36
  ' Settings are kept in finetune/loop.json; start again to resume where it stopped.',
30
37
  ];
@@ -35,42 +42,74 @@ export const FINETUNE_DETAILS = [
35
42
  ' start Train every case: run, analyze, apply, run again. Resumes.',
36
43
  ' status Where it stands (the default). The detail: finetune/STATUS.md',
37
44
  ' stop Let the steps in flight finish, then stop the running tuning.',
45
+ ' serve Show the tuning live in a browser until Ctrl-C. start does this',
46
+ ' itself, and prints the address; serve is for when it is not running.',
38
47
  ' retry [ids...] Another round for difficult cases: --difficult takes them all,',
39
48
  ' ids and globs add to it, --class narrows. Run them with start.',
40
- " session <case> Print the command that resumes that case's analyze session.",
49
+ " session <case> Print the command that resumes the analyze session of that case's",
50
+ ' latest try.',
51
+ ' gate <apply dir> Judge an apply in progress as the loop will: bytes moved per',
52
+ ' agent, skill and file, any limit broken, and zen check.',
53
+ ' Exits 1 when it would be undone. Read-only.',
41
54
  '',
42
- 'Options for start:',
55
+ 'Which cases (start):',
56
+ ' --class <c> Only this class. Repeatable.',
57
+ ' --id <glob> Only cases whose id matches. Repeatable.',
58
+ ' --rubric yes|no Only cases with, or without, a rubric.',
59
+ ' --limit <n> Only the first n cases of the order. Not kept.',
60
+ ' --more <n> Take at most n cases - new, part-way or failed - then stop.',
61
+ ' Parked cases it took still finish. Not kept.',
62
+ '',
63
+ 'How it runs (start):',
43
64
  ' -N, --workers <n> Cases trained at once. Default 4.',
44
- ' -M, --apply-at <n> Apply once this many requests wait. Default: the workers.',
65
+ ' -M, --apply-at <n> Apply once this many cases wait for a fix. Default 4, min 2.',
45
66
  ' --tries <n> Tries without memory before a case is difficult. Default 4.',
46
67
  ' --mem-tries <n> Tries with memory. Default 3.',
47
68
  ' --merge-every <k> Merge the kept memories after k more arrive. Default 3.',
48
69
  ' --seed <n> Order of cases inside a class. Default 1.',
49
- ' --class <c> Only this class. Repeatable.',
50
- ' --id <glob> Only cases whose id matches. Repeatable.',
51
- ' --rubric yes|no Only cases with, or without, a rubric.',
52
- ' --limit <n> Only the first n cases of the order.',
53
- ' --force Start even though the dataset drifted from its sources.',
70
+ '',
71
+ 'What an apply may do (start):',
72
+ ' --budget <bytes> Most one apply may add to what every call reads. Default 1500.',
73
+ ' --max-growth <pct> Once that is this much over its size when the tuning began,',
74
+ ' an apply may add nothing. Default 50.',
75
+ ' --skill-cap <bytes> Most one skill may hold. Default 12000.',
76
+ ' --max-skills <n> Most skills one agent may see. Default 10.',
77
+ ' --recheck <n> After each apply, run this many completed cases again on it;',
78
+ ' one that fails both tries sends its fixes to the next apply.',
79
+ ' Default 3, 0 none.',
80
+ '',
81
+ 'Other (start):',
82
+ ' --force Start past dataset drift, and allow -M 1.',
54
83
  ' --plain No live view: print a line per start and stop, as in a pipe.',
55
84
  ' --project <name|dir> Which project. Default: the one you are in.',
56
85
  '',
57
86
  'One case: without memory, run it and /analyze it until the analysis says done -',
58
87
  'parking it until the next apply after each try that is not - then the same with',
59
- 'the memory the passing run wrote. Tries used up: the case is difficult.',
88
+ 'the memory the passing run wrote. Tries used up: the case is difficult. Each try',
89
+ 'has its own private memory; passing memories are merged into finetune/memories/.',
60
90
  '',
61
- 'No run shares a memory: each try has its own private directory. A case whose',
62
- 'with-memory run passes submits a copy of that memory; every k submissions are',
63
- 'merged, with the last merge, into finetune/memories/merged/mNN - numbered,',
64
- 'never rewritten, each listing the cases it holds.',
91
+ 'A parked case gives its worker back; woken cases take a worker before new ones.',
92
+ '/finetune-apply fixes the parked cases together: when -M wait, or once nothing',
93
+ 'else can run. No run starts during an apply. What it may do:',
94
+ ' - Every call reads the house rules, the prompt, preloaded skills and one line per',
95
+ ' skill, so that is what --budget counts. A skill body is paid only once loaded:',
96
+ ' moving a procedure from a prompt into a skill is a saving.',
97
+ ' - One case is an anecdote. A new rule needs two cases asking for it; one is',
98
+ ' deferred and offered to every later apply until a second asks.',
99
+ ' - A skill made for a case must be loaded by its next run, or the next apply',
100
+ ' fixes the description.',
101
+ ' - It tidies up after itself: the files it touched lose their duplicates and',
102
+ ' contradictions in the same edit.',
103
+ ' - zen check must pass and the budget hold, or the edit is undone and tried once',
104
+ ' more. A second zen check failure stops the tuning; a second budget failure only',
105
+ ' undoes the apply.',
106
+ 'Every apply is a new version of agents.yaml + agents/. Then --recheck completed',
107
+ 'cases run again on it, as ordinary work - nothing waits. One that fails both tries',
108
+ 'has regressed: its fixes join the queue, and the next apply repairs what broke.',
65
109
  '',
66
- 'A parked case gives its worker back, so another case runs meanwhile; woken cases',
67
- 'take a worker before new ones. Requests are applied in batches by /finetune-apply:',
68
- 'when -M cases are parked, or once nothing is running and no case is left to start.',
69
- 'No run starts during an apply. zen check must pass,',
70
- 'or the edit is undone. Every apply is a new version of agents.yaml + agents/.',
71
- '',
72
- 'A case is difficult when its tries run out and the analysis still says no; that is',
73
- 'final for its revision until `retry` gives it another round on the prose of the day.',
110
+ 'A case is difficult when its tries run out and the analysis still says no, or at',
111
+ 'once when all an apply could say of it is structural - a tool, a skill binding,',
112
+ 'the dataset. That is final for its revision until `retry` gives it another round.',
74
113
  'A case fails instead when something outside the prose broke - the sandbox, a',
75
114
  'provider, a key: it keeps its tries and carries on from that step at the next start.',
76
115
  `After ${FAILURES_IN_A_ROW} failures in a row the tuning stops, and says what broke;`,
@@ -84,6 +123,8 @@ export const FINETUNE_DETAILS = [
84
123
  'Examples:',
85
124
  ' zen meta finetune start -N 4',
86
125
  ' zen meta finetune start --class search --limit 6 -N 2 -M 2',
126
+ ' zen meta finetune start --limit 4 --recheck 0 # quick, cheap',
127
+ ' zen meta finetune start --more 4',
87
128
  ' zen meta finetune',
88
129
  ' zen meta finetune retry --difficult --class merge',
89
130
  ' zen meta finetune session plan-day',
@@ -96,10 +137,16 @@ const OPTIONS = {
96
137
  'mem-tries': { type: 'string' },
97
138
  'merge-every': { type: 'string' },
98
139
  seed: { type: 'string' },
140
+ recheck: { type: 'string' },
141
+ budget: { type: 'string' },
142
+ 'max-growth': { type: 'string' },
143
+ 'skill-cap': { type: 'string' },
144
+ 'max-skills': { type: 'string' },
99
145
  class: { type: 'string', multiple: true },
100
146
  id: { type: 'string', multiple: true },
101
147
  rubric: { type: 'string' },
102
148
  limit: { type: 'string' },
149
+ more: { type: 'string' },
103
150
  force: { type: 'boolean' },
104
151
  plain: { type: 'boolean' },
105
152
  difficult: { type: 'boolean' },
@@ -119,12 +166,16 @@ export async function runFinetune(ctx, project, options = {}) {
119
166
  return status(ctx, project);
120
167
  case 'stop':
121
168
  return stop(project);
169
+ case 'serve':
170
+ return serve(ctx, project);
122
171
  case 'retry':
123
172
  return retryCases(ctx, project, values, rest);
124
173
  case 'session':
125
174
  return session(project, rest);
175
+ case 'gate':
176
+ return gate(ctx, project, rest, options);
126
177
  default:
127
- throw usageError(`unknown finetune verb: ${verb}`, 'one of: start, status, stop, retry, session');
178
+ throw usageError(`unknown finetune verb: ${verb}`, 'one of: start, status, stop, serve, retry, session, gate');
128
179
  }
129
180
  }
130
181
  /**
@@ -163,10 +214,9 @@ function whole(text, flag, min = 1) {
163
214
  const configFile = (dir) => join(dir, FINETUNE_DIR, 'loop.json');
164
215
  export function readConfig(dir) {
165
216
  try {
166
- return {
167
- ...DEFAULTS,
168
- ...JSON.parse(readFileSync(configFile(dir), 'utf8')),
169
- };
217
+ // A limit is for one start; older versions kept it, and had a guard and consolidations.
218
+ const { limit: _, guard, consolidateEvery: __, ...saved } = JSON.parse(readFileSync(configFile(dir), 'utf8'));
219
+ return { ...DEFAULTS, ...(guard !== undefined ? { recheck: guard } : {}), ...saved };
170
220
  }
171
221
  catch {
172
222
  return { ...DEFAULTS };
@@ -178,22 +228,32 @@ function configFrom(dir, v) {
178
228
  if (v.rubric !== undefined && v.rubric !== 'yes' && v.rubric !== 'no') {
179
229
  throw usageError('--rubric takes yes or no');
180
230
  }
231
+ const applyAt = whole(v['apply-at'], '-M');
232
+ if (applyAt === 1 && !v.force) {
233
+ throw usageError('-M 1 applies what one case asked for, alone', 'one case is an anecdote: use -M 2 or more, or pass --force');
234
+ }
181
235
  const config = {
182
236
  ...saved,
183
237
  workers,
184
- applyAt: whole(v['apply-at'], '-M') ?? (v.workers ? workers : saved.applyAt),
238
+ // An older version saved -N as -M, so a tuning on one worker kept applying one case at a time.
239
+ applyAt: applyAt ?? (saved.applyAt < 2 && !v.force ? 2 : saved.applyAt),
185
240
  tries: whole(v.tries, '--tries') ?? saved.tries,
186
241
  memTries: whole(v['mem-tries'], '--mem-tries') ?? saved.memTries,
187
242
  mergeEvery: whole(v['merge-every'], '--merge-every') ?? saved.mergeEvery,
188
243
  seed: whole(v.seed, '--seed', 0) ?? saved.seed,
244
+ recheck: whole(v.recheck, '--recheck', 0) ?? saved.recheck,
245
+ budget: whole(v.budget, '--budget', 0) ?? saved.budget,
246
+ maxGrowth: whole(v['max-growth'], '--max-growth', 0) ?? saved.maxGrowth,
247
+ skillCap: whole(v['skill-cap'], '--skill-cap') ?? saved.skillCap,
248
+ maxSkills: whole(v['max-skills'], '--max-skills') ?? saved.maxSkills,
189
249
  ...(v.class ? { classes: v.class } : {}),
190
250
  ...(v.id ? { ids: v.id } : {}),
191
251
  ...(v.rubric ? { rubric: v.rubric === 'yes' } : {}),
192
- ...(v.limit ? { limit: whole(v.limit, '--limit') } : {}),
193
252
  };
253
+ const limit = whole(v.limit, '--limit');
194
254
  mkdirSync(join(dir, FINETUNE_DIR), { recursive: true });
195
255
  writeFileSync(configFile(dir), `${JSON.stringify(config, null, 2)}\n`);
196
- return config;
256
+ return limit ? { ...config, limit } : config;
197
257
  }
198
258
  async function start(ctx, project, values, options) {
199
259
  const store = DatasetStore.open(project.dir);
@@ -204,9 +264,11 @@ async function start(ctx, project, values, options) {
204
264
  if (!report.clean && !values.force) {
205
265
  throw invalidError(`the dataset has drifted from its sources (${report.cases.length} case(s), ${report.uncovered.length} section(s))`, 'refresh it: zen meta run /dataset - or start with --force');
206
266
  }
267
+ const more = whole(values.more, '--more');
207
268
  const config = configFrom(project.dir, values);
208
269
  const children = new Set();
209
270
  const t = new Tuning(project.dir, config, options.zen ?? spawnZen(project.dir, children), options.tickMs);
271
+ t.more = more;
210
272
  if (!options.zen) {
211
273
  editorFiles(project.dir);
212
274
  }
@@ -233,11 +295,15 @@ async function start(ctx, project, values, options) {
233
295
  };
234
296
  process.on('SIGINT', onInterrupt);
235
297
  note(`${bold('tuning')} ${project.name} · ${config.workers} workers · apply at ${config.applyAt}`);
236
- note(dim(`follow it: file://${join(t.dir, 'STATUS.html')}`));
298
+ const viewing = options.zen ? undefined : await viewOf(project.dir);
299
+ const url = viewing?.url ?? liveView(project.dir);
300
+ if (url) {
301
+ note(dim(`follow it: ${url}`));
302
+ }
237
303
  let close;
238
304
  if (!ctx.json && !values.plain && !options.zen && process.stderr.isTTY && process.stdin.isTTY) {
239
305
  const { watch } = await import("./tui.js");
240
- close = await watch(t, onInterrupt);
306
+ close = await watch(t, onInterrupt, url);
241
307
  drawing = true;
242
308
  }
243
309
  try {
@@ -246,6 +312,7 @@ async function start(ctx, project, values, options) {
246
312
  finally {
247
313
  close?.();
248
314
  process.off('SIGINT', onInterrupt);
315
+ await viewing?.close();
249
316
  }
250
317
  if (!ctx.json) {
251
318
  await status(ctx, project);
@@ -276,12 +343,13 @@ function status(ctx, project) {
276
343
  }
277
344
  const holder = liveHolder(join(dir, 'loop.lock'));
278
345
  const c = counts(project.dir);
346
+ const url = liveView(project.dir);
279
347
  if (ctx.json) {
280
348
  return json({
281
349
  running: Boolean(holder),
282
350
  ...c,
283
351
  status: join(dir, 'STATUS.md'),
284
- html: join(dir, 'STATUS.html'),
352
+ ...(url ? { url } : {}),
285
353
  });
286
354
  }
287
355
  write(`${bold(project.name)} ${holder ? green(`running (pid ${holder.pid})`) : dim('not running')}`);
@@ -293,9 +361,51 @@ function status(ctx, project) {
293
361
  note(dim(`another round for the difficult ones: ${cyan(`zen meta ${project.name} finetune retry --difficult`)}`));
294
362
  }
295
363
  if (existsSync(join(dir, 'STATUS.md'))) {
296
- note(dim(`the detail: ${cyan(join(dir, 'STATUS.md'))} · ${cyan(`file://${join(dir, 'STATUS.html')}`)}`));
364
+ note(dim(`the detail: ${cyan(join(dir, 'STATUS.md'))} · ${url ? cyan(url) : `live in a browser: ${cyan(`zen meta ${project.name} finetune serve`)}`}`));
297
365
  }
298
366
  }
367
+ /** The live view for a tuning, or a note saying why there is none - it never stops one. */
368
+ async function viewOf(projectDir) {
369
+ if (liveView(projectDir)) {
370
+ return undefined;
371
+ }
372
+ try {
373
+ return await startView(projectDir);
374
+ }
375
+ catch (err) {
376
+ note(yellow(`no live view: ${err instanceof Error ? err.message : String(err)}`));
377
+ return undefined;
378
+ }
379
+ }
380
+ async function serve(ctx, project) {
381
+ const live = liveView(project.dir);
382
+ if (live) {
383
+ if (ctx.json) {
384
+ return json({ url: live, served: false });
385
+ }
386
+ write(live);
387
+ note(dim('already served by another zen process'));
388
+ return;
389
+ }
390
+ const viewing = await startView(project.dir);
391
+ if (ctx.json) {
392
+ json({ url: viewing.url, served: true });
393
+ }
394
+ else {
395
+ write(viewing.url);
396
+ note(dim('serving the tuning · Ctrl-C stops it'));
397
+ }
398
+ await new Promise((settle) => {
399
+ const done = () => {
400
+ process.off('SIGINT', done);
401
+ process.off('SIGTERM', done);
402
+ settle();
403
+ };
404
+ process.once('SIGINT', done);
405
+ process.once('SIGTERM', done);
406
+ });
407
+ await viewing.close();
408
+ }
299
409
  function stop(project) {
300
410
  const dir = join(project.dir, FINETUNE_DIR);
301
411
  if (!liveHolder(join(dir, 'loop.lock'))) {
@@ -329,10 +439,78 @@ function session(project, rest) {
329
439
  if (!c) {
330
440
  throw invalidError(`no case ${id}`);
331
441
  }
332
- const file = join(project.dir, FINETUNE_DIR, 'cases', id, `r${c.rev}`, 'session');
333
- if (!existsSync(file)) {
442
+ const t = new Tuning(project.dir, readConfig(project.dir), async () => ({
443
+ code: 1,
444
+ stdout: '',
445
+ }));
446
+ const latest = attemptsOf(t, c)
447
+ .reverse()
448
+ .map((a) => analyzeSession(a.dir))
449
+ .find(Boolean);
450
+ if (!latest) {
334
451
  throw invalidError(`${id} has not been analyzed yet at revision ${c.rev}`);
335
452
  }
336
- write(`zen meta resume ${project.name} ${readFileSync(file, 'utf8').trim()}`);
453
+ write(`zen meta resume ${project.name} ${latest}`);
454
+ }
455
+ /** The gates `#edit` runs after the apply agent exits, run now against the same `before` and limits. */
456
+ async function gate(ctx, project, rest, options) {
457
+ const [at] = rest;
458
+ if (!at) {
459
+ throw usageError('gate of which apply?', 'zen meta finetune gate <apply dir>');
460
+ }
461
+ const dir = resolve(ctx.cwd, at);
462
+ const g = readJson(join(dir, 'gate.json'));
463
+ if (!g || existsSync(join(dir, 'applied.json'))) {
464
+ throw invalidError(`${at} is not an apply in progress`, 'pass the apply directory named on the line after /finetune-apply');
465
+ }
466
+ const after = await measure(project.dir);
467
+ const over = overLimits(g.before, after, g.limits);
468
+ const log = join(dir, 'gate.log');
469
+ const check = await (options.zen ?? checkOnce(project.dir))(['check', '--no-models', '--no-sandbox'], log);
470
+ const pass = over.length === 0 && check.code === 0;
471
+ const grew = after.total - g.before.total;
472
+ if (ctx.json) {
473
+ json({
474
+ pass,
475
+ alwaysRead: { before: g.before.total, after: after.total },
476
+ limits: g.limits,
477
+ over,
478
+ check: { code: check.code, log },
479
+ });
480
+ }
481
+ else {
482
+ const moved = costChanges(g.before, after);
483
+ write([
484
+ `always read, summed over the agents: ${g.before.total} -> ${after.total} bytes (${grew > 0 ? '+' : ''}${grew}); budget +${g.limits.budget}, ceiling ${g.limits.ceiling}`,
485
+ '',
486
+ ...(moved.length > 0 ? moved : ['nothing the agents read changed size']),
487
+ '',
488
+ ...(over.length > 0 ? ['over the limits:', ...over.map((o) => `- ${o}`), ''] : []),
489
+ check.code === 0
490
+ ? 'zen check: passed'
491
+ : `zen check: failed (exit ${check.code})\n${tail(log)}`,
492
+ pass ? 'gate: pass' : 'gate: fail - this apply would be undone',
493
+ ].join('\n'));
494
+ }
495
+ if (!pass) {
496
+ throw new CliError(over.length > 0 ? 'over the limits' : 'zen check failed', EXIT.failed, over.length > 0 ? 'cut, move or extract, then run gate again' : undefined);
497
+ }
498
+ }
499
+ const tail = (log) => existsSync(log) ? readFileSync(log, 'utf8').trimEnd().split('\n').slice(-30).join('\n') : '';
500
+ /** `zen check` once, its output to `log` - not the loop's spawn, which records its children. */
501
+ function checkOnce(root) {
502
+ return async (args, log) => {
503
+ const fd = openSync(log, 'w');
504
+ try {
505
+ const res = spawnSync(process.execPath, [process.argv[1], ...args], {
506
+ cwd: root,
507
+ stdio: ['ignore', fd, fd],
508
+ });
509
+ return { code: res.status ?? 1, stdout: '' };
510
+ }
511
+ finally {
512
+ closeSync(fd);
513
+ }
514
+ };
337
515
  }
338
516
  //# sourceMappingURL=command.js.map
@@ -0,0 +1,46 @@
1
+ import { type ProjectReport } from '../host.ts';
2
+ import type { Config } from './tuning.ts';
3
+ export interface Cost {
4
+ /** per agent: house rules, prompt, preloaded skills, skill index lines */
5
+ alwaysRead: Record<string, number>;
6
+ /** `alwaysRead` summed over the agents */
7
+ total: number;
8
+ /** per skill: its SKILL.md */
9
+ skills: Record<string, number>;
10
+ /** per agent: the skills it can see */
11
+ visible: Record<string, number>;
12
+ /** every markdown file under agents/ */
13
+ files: Record<string, number>;
14
+ }
15
+ export interface Limits {
16
+ /** bytes one apply may add to `total` */
17
+ budget: number;
18
+ /** above this `total`, an apply may add nothing */
19
+ ceiling: number;
20
+ skillCap: number;
21
+ maxSkills: number;
22
+ }
23
+ /** `gate.json` in an apply's folder: what its budget gate compares against, kept for `finetune gate`. */
24
+ export interface Gate {
25
+ before: Cost;
26
+ limits: Limits;
27
+ }
28
+ export declare function measure(root: string): Promise<Cost>;
29
+ export interface Binding {
30
+ discovery: string;
31
+ allow?: string[];
32
+ preload?: string[];
33
+ }
34
+ /** No `skills:` key is the whole catalog as an index, the same as the runtime. */
35
+ export declare const bindingOf: (a: ProjectReport["agents"][number]) => Binding;
36
+ /** The skills a binding lets its agent load. */
37
+ export declare const visibleTo: <S extends {
38
+ name: string;
39
+ }>(b: Binding, entries: S[]) => S[];
40
+ export declare function costOf(root: string, report: ProjectReport): Cost;
41
+ export declare function limits(start: Cost, c: Config): Limits;
42
+ /** Every limit an edit broke, said so the next attempt can act on it; empty when none. */
43
+ export declare function overLimits(before: Cost, after: Cost, l: Limits): string[];
44
+ /** What moved between two measures, per agent, skill and file - for a person and the next attempt. */
45
+ export declare function costChanges(before: Cost, after: Cost): string[];
46
+ //# sourceMappingURL=cost.d.ts.map
@@ -0,0 +1,105 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { validateProject } from "../host.js";
4
+ export async function measure(root) {
5
+ return costOf(root, await validateProject({ dir: root }));
6
+ }
7
+ /** No `skills:` key is the whole catalog as an index, the same as the runtime. */
8
+ export const bindingOf = (a) => a.skills ?? { discovery: 'index' };
9
+ /** The skills a binding lets its agent load. */
10
+ export const visibleTo = (b, entries) => entries.filter((s) => !b.allow || b.allow.includes(s.name));
11
+ function size(root, rel) {
12
+ try {
13
+ return statSync(join(root, rel)).size;
14
+ }
15
+ catch {
16
+ return 0;
17
+ }
18
+ }
19
+ export function costOf(root, report) {
20
+ const entries = report.skills.entries;
21
+ const skills = Object.fromEntries(entries.map((s) => [s.name, size(root, s.path)]));
22
+ const alwaysRead = {};
23
+ const visible = {};
24
+ for (const a of report.agents) {
25
+ const binding = bindingOf(a);
26
+ const seen = visibleTo(binding, entries);
27
+ const index = binding.discovery === 'index'
28
+ ? seen.reduce((n, s) => n + Buffer.byteLength(`- ${s.name}: ${s.description}\n`), 0)
29
+ : 0;
30
+ const preloaded = (binding.preload ?? []).reduce((n, s) => n + (skills[s] ?? 0), 0);
31
+ alwaysRead[a.name] =
32
+ a.instructions.reduce((n, rel) => n + size(root, rel), 0) + preloaded + index;
33
+ visible[a.name] = seen.length;
34
+ }
35
+ const total = Object.values(alwaysRead).reduce((n, b) => n + b, 0);
36
+ return { alwaysRead, total, skills, visible, files: markdown(root) };
37
+ }
38
+ function markdown(root) {
39
+ const out = {};
40
+ let names = [];
41
+ try {
42
+ names = readdirSync(join(root, 'agents'), { recursive: true });
43
+ }
44
+ catch {
45
+ return out;
46
+ }
47
+ for (const name of names.sort()) {
48
+ if (name.endsWith('.md')) {
49
+ const rel = join('agents', name).split('\\').join('/');
50
+ out[rel] = size(root, rel);
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ export function limits(start, c) {
56
+ return {
57
+ budget: c.budget,
58
+ // A project that starts nearly empty still gets one apply's worth of room.
59
+ ceiling: Math.max(start.total + c.budget, Math.round(start.total * (1 + c.maxGrowth / 100))),
60
+ skillCap: c.skillCap,
61
+ maxSkills: c.maxSkills,
62
+ };
63
+ }
64
+ /** Every limit an edit broke, said so the next attempt can act on it; empty when none. */
65
+ export function overLimits(before, after, l) {
66
+ const out = [];
67
+ const grew = after.total - before.total;
68
+ if (grew > l.budget) {
69
+ out.push(`what the agents always read grew by ${grew} bytes (${before.total} -> ${after.total}), over the budget of ${l.budget}`);
70
+ }
71
+ if (grew > 0 && after.total > l.ceiling) {
72
+ out.push(`what the agents always read is ${after.total} bytes, over the ceiling of ${l.ceiling}: past it an apply may add nothing - cut, move or extract to make room`);
73
+ }
74
+ for (const [name, bytes] of Object.entries(after.skills)) {
75
+ if (bytes > l.skillCap && bytes > (before.skills[name] ?? 0)) {
76
+ out.push(`skill ${name} holds ${bytes} bytes, over the cap of ${l.skillCap}: split it by when each part is needed`);
77
+ }
78
+ }
79
+ for (const [agent, n] of Object.entries(after.visible)) {
80
+ if (n > l.maxSkills && n > (before.visible[agent] ?? 0)) {
81
+ out.push(`agent ${agent} sees ${n} skills, over ${l.maxSkills}: merge skills whose descriptions overlap`);
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+ /** What moved between two measures, per agent, skill and file - for a person and the next attempt. */
87
+ export function costChanges(before, after) {
88
+ const rows = [];
89
+ const diff = (label, a, b) => {
90
+ for (const key of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()) {
91
+ const was = a[key] ?? 0;
92
+ const now = b[key] ?? 0;
93
+ if (was !== now) {
94
+ rows.push(`| ${label} | ${key} | ${was} | ${now} | ${now - was > 0 ? '+' : ''}${now - was} |`);
95
+ }
96
+ }
97
+ };
98
+ diff('always read', before.alwaysRead, after.alwaysRead);
99
+ diff('skill', before.skills, after.skills);
100
+ diff('file', before.files, after.files);
101
+ return rows.length > 0
102
+ ? ['| What | Name | Before | After | Change |', '| --- | --- | --- | --- | --- |', ...rows]
103
+ : [];
104
+ }
105
+ //# sourceMappingURL=cost.js.map
@@ -87,10 +87,12 @@ export interface NoteRow {
87
87
  caseRev: number;
88
88
  run?: string;
89
89
  verdict?: Verdict;
90
- rubric?: Record<string, 'pass' | 'fail'>;
90
+ rubric?: Record<string, Grade>;
91
91
  text: string;
92
92
  by: By;
93
93
  }
94
+ /** `invalid`: the rubric line itself is broken - it says how one implementation did it, not what is right. */
95
+ export type Grade = 'pass' | 'fail' | 'invalid';
94
96
  export type JournalRow = ChangeRow | NoteRow;
95
97
  export interface Ignored {
96
98
  file: string;
@@ -1,4 +1,4 @@
1
- import { type Verdict } from './dataset/types.ts';
1
+ import { type Grade, type Verdict } from './dataset/types.ts';
2
2
  export type Phase = 'nomem' | 'mem';
3
3
  export interface Improvement {
4
4
  id: string;
@@ -18,14 +18,26 @@ export interface Feedback {
18
18
  phase: Phase;
19
19
  attempt: number;
20
20
  verdict: Verdict;
21
- rubric: Record<string, 'pass' | 'fail'>;
21
+ rubric: Record<string, Grade>;
22
22
  /** right, and nothing left worth changing */
23
23
  done: boolean;
24
24
  summary: string;
25
25
  improvements: Improvement[];
26
26
  /** what outside the project made the run void: the sandbox, the network, a provider */
27
27
  infra?: string;
28
+ /** a recheck's: the apply whose version the case failed on, after passing before it */
29
+ recheck?: string;
30
+ /** the try's folder, when it is not the case's own attempt folder */
31
+ dir?: string;
28
32
  }
33
+ /** A summary's first paragraph - the verdict, for a table cell or a one-line note. */
34
+ export declare const lead: (summary: string) => string;
35
+ /** A `specification` improvement is for the dataset, never for an apply: prose cannot fix a rubric. */
36
+ export declare const forDataset: (i: Improvement) => boolean;
37
+ /** The improvements an apply can act on. */
38
+ export declare const forProse: (f: {
39
+ improvements: Improvement[];
40
+ }) => Improvement[];
29
41
  export declare const feedbackId: (caseId: string, phase: Phase, attempt: number, round?: number) => string;
30
42
  /**
31
43
  * The feedback, or what is wrong with the file - said so a model can fix it.