@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
@@ -29,13 +29,19 @@ export function resultOf(t, c) {
29
29
  return r;
30
30
  }
31
31
  export function nextCase(t) {
32
+ if (t.more !== undefined && t.tried.size >= t.more) {
33
+ return undefined;
34
+ }
32
35
  return selection(t).find((c) => {
33
36
  if (t.claimed.has(c.id)) {
34
37
  return false;
35
38
  }
36
39
  const r = resultOf(t, c);
37
- // Once per start: a sandbox that is down would only fail it again at once.
38
- return !r || (r.state === 'failed' && !t.tried.has(c.id));
40
+ if (r) {
41
+ // Once per start: a sandbox that is down would only fail it again at once.
42
+ return r.state === 'failed' && !t.tried.has(c.id);
43
+ }
44
+ return true;
39
45
  });
40
46
  }
41
47
  //# sourceMappingURL=sampler.js.map
@@ -24,14 +24,18 @@ export interface Run {
24
24
  tokens?: ByModel;
25
25
  metrics?: RunMetrics;
26
26
  durationMs?: number;
27
+ /** skills an apply made for this case that the run never loaded */
28
+ missedSkills?: string[];
27
29
  }
28
30
  export declare class VoidRun extends StepError {
29
31
  }
30
32
  /** The container engine failed: every run after this one would fail the same way. */
31
33
  export declare class SandboxDown extends StepError {
32
34
  }
33
- /** The case keeps one analyze session per revision, so each analysis remembers the last. */
34
- export declare function sessionOf(t: Tuning, c: Case): string | undefined;
35
+ /** One `zen run`. Writes run.json, or says why the run was void. */
36
+ export declare function runOnce(t: Tuning, c: Case, dir: string, memory?: string, expect?: string[]): Promise<string | undefined>;
37
+ /** The meta agent's answer is the human report: kept as analysis.md beside the feedback. A retry keeps its own envelope, so its tokens are counted too. */
38
+ export declare function keepAnalysis(dir: string, stdout: string, replace?: boolean): void;
35
39
  export declare function markCompleted(t: Tuning, c: Case): void;
36
40
  export declare function markDifficult(t: Tuning, c: Case, phase: Phase, reason: string): void;
37
41
  /** Something outside the prose broke. The case keeps its tries and runs again at the next start. */
@@ -4,11 +4,12 @@ import { hostname } from 'node:os';
4
4
  import { basename, dirname, join } from 'node:path';
5
5
  import { EXIT } from "../host.js";
6
6
  import { sessionKept } from "../tokens.js";
7
+ import { writeAnalyzeBrief } from "./analyze-brief.js";
7
8
  import { batchInput } from "./dataset/export.js";
8
9
  import { DatasetStore } from "./dataset/store.js";
9
10
  import { errorIn, sizeOf, StepError } from "./failure.js";
10
- import { readFeedback } from "./feedback.js";
11
- import { caseTokens, writeFeedback } from "./report.js";
11
+ import { feedbackId, forDataset, forProse, lead, readFeedback, } from "./feedback.js";
12
+ import { attemptsOf, caseTokens, rubricScore, writeFeedback } from "./report.js";
12
13
  import { readJson } from "./tuning.js";
13
14
  import { envelopesIn, runMetrics, spent, tokensOf, } from "./usage.js";
14
15
  // ---------------------------------------------------------------------------
@@ -27,8 +28,15 @@ export async function train(t, s, c) {
27
28
  passed = run;
28
29
  break;
29
30
  }
31
+ const rubricOnly = datasetOnly(feedback);
32
+ if (rubricOnly) {
33
+ return markDifficult(t, c, 'nomem', rubricOnly);
34
+ }
30
35
  if (attempt < t.config.tries) {
31
- await t.improvements.apply(feedback, s);
36
+ const { structural } = await t.improvements.apply(feedback, s);
37
+ if (structural) {
38
+ return markDifficult(t, c, 'nomem', `structural: ${structural}`);
39
+ }
32
40
  }
33
41
  }
34
42
  if (!passed) {
@@ -42,8 +50,15 @@ export async function train(t, s, c) {
42
50
  t.memories.submit(c, attempt, run);
43
51
  break;
44
52
  }
53
+ const rubricOnly = datasetOnly(feedback);
54
+ if (rubricOnly) {
55
+ return markDifficult(t, c, 'mem', rubricOnly);
56
+ }
45
57
  if (attempt < t.config.memTries) {
46
- await t.improvements.apply(feedback, s);
58
+ const { structural } = await t.improvements.apply(feedback, s);
59
+ if (structural) {
60
+ return markDifficult(t, c, 'mem', `structural: ${structural}`);
61
+ }
47
62
  }
48
63
  }
49
64
  if (!cheaper) {
@@ -51,6 +66,14 @@ export async function train(t, s, c) {
51
66
  }
52
67
  markCompleted(t, c);
53
68
  }
69
+ /** Not done, and every finding is about the case itself: another try on the same prose learns nothing. */
70
+ function datasetOnly(f) {
71
+ const dataset = f.improvements.filter(forDataset);
72
+ if (dataset.length === 0 || forProse(f).length > 0) {
73
+ return undefined;
74
+ }
75
+ return `specification: ${dataset.map((i) => lead(i.change)).join('; ')}`;
76
+ }
54
77
  /** A run its analysis finds void is set aside and run again: it says nothing about the prose. */
55
78
  async function tryOnce(t, s, c, phase, attempt, memory) {
56
79
  for (let voids = 1;; voids++) {
@@ -97,8 +120,12 @@ async function zenRun(t, s, c, phase, attempt, memory) {
97
120
  t.at(s, phase, attempt, 'run');
98
121
  t.improvements.runs++;
99
122
  const began = Date.now();
123
+ // The skills the apply after the last try made for this case: this run should load them.
124
+ const expect = attempt > 1
125
+ ? t.improvements.skillsFor(feedbackId(c.id, phase, attempt - 1, t.round(c)))
126
+ : [];
100
127
  try {
101
- const why = await t.journal.step({ kind: 'run', dir }, () => runOnce(t, c, dir, memory));
128
+ const why = await t.journal.step({ kind: 'run', dir }, () => runOnce(t, c, dir, memory, expect));
102
129
  const ran = why ? undefined : readJson(file);
103
130
  t.ended(s, {
104
131
  what: why ? 'void' : 'ran',
@@ -116,7 +143,7 @@ async function zenRun(t, s, c, phase, attempt, memory) {
116
143
  return readJson(file);
117
144
  }
118
145
  /** One `zen run`. Writes run.json, or says why the run was void. */
119
- async function runOnce(t, c, dir, memory) {
146
+ export async function runOnce(t, c, dir, memory, expect = []) {
120
147
  mkdirSync(dir, { recursive: true });
121
148
  const mem = join(dir, 'memory');
122
149
  rmSync(mem, { recursive: true, force: true });
@@ -180,6 +207,7 @@ async function runOnce(t, c, dir, memory) {
180
207
  if (metrics?.sandbox) {
181
208
  throw new SandboxDown(metrics.sandbox, join(runDir, 'graph.mmd'));
182
209
  }
210
+ const missed = expect.filter((name) => !metrics?.skills.includes(name));
183
211
  const run = {
184
212
  dir: runDir,
185
213
  memory: mem,
@@ -192,6 +220,7 @@ async function runOnce(t, c, dir, memory) {
192
220
  ...(meta?.usage ? { usage: meta.usage } : {}),
193
221
  ...(metrics ? { tokens: tokensOf(metrics), metrics } : {}),
194
222
  ...(meta?.durationMs !== undefined ? { durationMs: meta.durationMs } : {}),
223
+ ...(missed.length > 0 ? { missedSkills: missed } : {}),
195
224
  };
196
225
  writeFileSync(join(dir, 'run.json'), `${JSON.stringify(run, null, 2)}\n`);
197
226
  return undefined;
@@ -199,11 +228,7 @@ async function runOnce(t, c, dir, memory) {
199
228
  // ---------------------------------------------------------------------------
200
229
  // zen meta run /analyze
201
230
  // ---------------------------------------------------------------------------
202
- /** The case keeps one analyze session per revision, so each analysis remembers the last. */
203
- export function sessionOf(t, c) {
204
- const file = join(t.caseDir(c), 'session');
205
- return existsSync(file) ? readFileSync(file, 'utf8').trim() || undefined : undefined;
206
- }
231
+ /** One analyze session per try, kept in its folder: the brief carries the earlier tries. */
207
232
  async function analyze(t, s, c, phase, attempt, run) {
208
233
  const dir = t.attemptDir(c, phase, attempt);
209
234
  const file = join(dir, 'feedback.json');
@@ -225,7 +250,7 @@ async function analyze(t, s, c, phase, attempt, run) {
225
250
  attempt,
226
251
  result: feedback.verdict === 'void'
227
252
  ? `void: ${feedback.infra}`
228
- : `${feedback.verdict}${graded.length ? ` ${graded.filter((g) => g === 'pass').length}/${graded.length}` : ''}${feedback.done ? ', done' : ''}`,
253
+ : `${feedback.verdict}${graded.length ? ` ${rubricScore(feedback)}` : ''}${feedback.done ? ', done' : ''}`,
229
254
  took: Date.now() - began,
230
255
  tokens: spent(envelopesIn(dir, 'analyze')),
231
256
  });
@@ -241,12 +266,18 @@ async function analyze(t, s, c, phase, attempt, run) {
241
266
  async function analyzeOnce(t, s, c, phase, attempt, run, file) {
242
267
  const dir = dirname(file);
243
268
  const known = t.known(c, phase, attempt);
244
- let session = sessionOf(t, c);
245
- if (!session) {
246
- session = randomUUID();
247
- mkdirSync(t.caseDir(c), { recursive: true });
248
- writeFileSync(join(t.caseDir(c), 'session'), `${session}\n`);
249
- }
269
+ let session = randomUUID();
270
+ writeFileSync(join(dir, 'session'), `${session}\n`);
271
+ const all = attemptsOf(t, c);
272
+ const at = all.findIndex((a) => a.phase === phase && a.attempt === attempt);
273
+ const brief = await writeAnalyzeBrief(t, dir, {
274
+ run,
275
+ case: c,
276
+ phase,
277
+ attempt,
278
+ earlier: at >= 0 ? all.slice(0, at) : all,
279
+ ...(run.missedSkills?.length ? { missed: run.missedSkills } : {}),
280
+ });
250
281
  const hash = t.system.hash();
251
282
  const applies = t.improvements.started;
252
283
  const events = join(dir, 'analyze.events.jsonl');
@@ -264,6 +295,8 @@ async function analyzeOnce(t, s, c, phase, attempt, run, file) {
264
295
  const from = sizeOf(log);
265
296
  const res = await t.zen([
266
297
  ...continuing(),
298
+ '--prompt-file',
299
+ brief,
267
300
  '/analyze',
268
301
  run.dir,
269
302
  `case=${c.id}`,
@@ -271,13 +304,14 @@ async function analyzeOnce(t, s, c, phase, attempt, run, file) {
271
304
  `phase=${phase}`,
272
305
  `attempt=${attempt}`,
273
306
  `feedback=${file}`,
307
+ ...(run.missedSkills?.length ? [`missed-skills=${run.missedSkills.join(',')}`] : []),
274
308
  ], log);
275
309
  keepAnalysis(dir, res.stdout);
276
310
  // `zen meta run` starts a new session when the meta model changed since this one; follow it.
277
311
  const started = sessionIn(res.stdout);
278
312
  if (started && started !== session) {
279
313
  session = started;
280
- writeFileSync(join(t.caseDir(c), 'session'), `${session}\n`);
314
+ writeFileSync(join(dir, 'session'), `${session}\n`);
281
315
  }
282
316
  const rubricIds = c.rubric.map((r) => r.id);
283
317
  let read = readFeedback(file, known, rubricIds);
@@ -319,7 +353,7 @@ function sessionIn(stdout) {
319
353
  }
320
354
  }
321
355
  /** The meta agent's answer is the human report: kept as analysis.md beside the feedback. A retry keeps its own envelope, so its tokens are counted too. */
322
- function keepAnalysis(dir, stdout, replace = true) {
356
+ export function keepAnalysis(dir, stdout, replace = true) {
323
357
  let envelope;
324
358
  try {
325
359
  envelope = JSON.parse(stdout);
@@ -345,7 +379,7 @@ function note(t, c, f, run, session) {
345
379
  run: run.dir,
346
380
  verdict: f.verdict,
347
381
  ...(Object.keys(rubric).length > 0 ? { rubric } : {}),
348
- text: f.verdict === 'void' ? `void: ${f.infra}` : f.summary || f.verdict,
382
+ text: f.verdict === 'void' ? `void: ${f.infra}` : lead(f.summary) || f.verdict,
349
383
  by: { session, prompt: 'analyze', host: hostname() },
350
384
  });
351
385
  }
@@ -8,9 +8,9 @@ export interface Part {
8
8
  }
9
9
  export type Line = Part[];
10
10
  /** The overview: under way, tokens, log - as many log rows as the height leaves. */
11
- export declare function mainLines(s: Snapshot, selected: number, width: number, height: number, theme: Theme): Line[];
11
+ export declare function mainLines(s: Snapshot, selected: number, width: number, height: number, theme: Theme, url?: string): Line[];
12
12
  /** One step's agent, line by line as it works. */
13
- export declare function followLines(s: Snapshot, key: string, width: number, height: number, theme: Theme): Line[];
13
+ export declare function followLines(s: Snapshot, key: string, width: number, height: number, theme: Theme, url?: string): Line[];
14
14
  /** Draws the tuning until the returned function is called. */
15
- export declare function watch(t: Tuning, interrupt: () => void): Promise<() => void>;
15
+ export declare function watch(t: Tuning, interrupt: () => void, url?: string): Promise<() => void>;
16
16
  //# sourceMappingURL=tui.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, render, Text, useInput, useWindowSize } from 'ink';
3
+ import { spawn } from 'node:child_process';
3
4
  import React, { useEffect, useState } from 'react';
4
5
  import { resolveTheme } from "../host.js";
5
6
  import { since } from "./report.js";
@@ -21,16 +22,28 @@ function footer(width, keys, theme) {
21
22
  [{ text: keys, color: theme.chrome.color }],
22
23
  ];
23
24
  }
24
- function keysFor(s, following) {
25
+ function keysFor(s, following, url) {
25
26
  if (s.state === 'stopping') {
26
27
  return 'stopping: the steps under way finish first · ctrl-c again kills them';
27
28
  }
28
29
  if (s.state !== 'running') {
29
30
  return s.state;
30
31
  }
32
+ const open = url ? 'o open in browser · ' : '';
31
33
  return following
32
- ? 'esc back · s stop · ctrl-c stop, twice kills'
33
- : 'up/down choose · enter follow · s stop · ctrl-c stop, twice kills';
34
+ ? `esc back · ${open}s stop · ctrl-c stop, twice kills`
35
+ : `up/down choose · enter follow · ${open}s stop · ctrl-c stop, twice kills`;
36
+ }
37
+ /** The platform opener, without a shell, so the address stays one argument. */
38
+ function openInBrowser(url) {
39
+ const command = process.platform === 'darwin'
40
+ ? 'open'
41
+ : process.platform === 'win32'
42
+ ? 'explorer'
43
+ : 'xdg-open';
44
+ spawn(command, [url], { stdio: 'ignore', detached: true })
45
+ .on('error', () => { })
46
+ .unref();
34
47
  }
35
48
  function header(s, theme) {
36
49
  const c = s.counts;
@@ -108,7 +121,7 @@ const logCells = (cells, width) => {
108
121
  return `${fit(where, 31)} ${fit(result, Math.max(10, width - 75))} ${fit(took, 7)} ${tokens}`;
109
122
  };
110
123
  /** The overview: under way, tokens, log - as many log rows as the height leaves. */
111
- export function mainLines(s, selected, width, height, theme) {
124
+ export function mainLines(s, selected, width, height, theme, url) {
112
125
  const quiet = theme.chrome.color;
113
126
  const out = [...header(s, theme)];
114
127
  if (s.now.length > 0) {
@@ -179,7 +192,7 @@ export function mainLines(s, selected, width, height, theme) {
179
192
  ]);
180
193
  }
181
194
  }
182
- return [...out, ...footer(width, keysFor(s, false), theme)];
195
+ return [...out, ...footer(width, keysFor(s, false, url), theme)];
183
196
  }
184
197
  function activity(e, theme) {
185
198
  const at = { text: `${clock(e.t)} `, color: theme.chrome.color };
@@ -204,10 +217,16 @@ function activity(e, theme) {
204
217
  ];
205
218
  case 'say':
206
219
  return [at, { text: 'says ', color: theme.accent }, { text: e.text }];
220
+ case 'think':
221
+ return [
222
+ at,
223
+ { text: 'thinks ', color: theme.accent },
224
+ { text: e.text, color: theme.chrome.color },
225
+ ];
207
226
  }
208
227
  }
209
228
  /** One step's agent, line by line as it works. */
210
- export function followLines(s, key, width, height, theme) {
229
+ export function followLines(s, key, width, height, theme, url) {
211
230
  const r = s.now.find((x) => x.key === key);
212
231
  const out = [];
213
232
  if (!r) {
@@ -230,9 +249,9 @@ export function followLines(s, key, width, height, theme) {
230
249
  out.push(activity(e, theme));
231
250
  }
232
251
  }
233
- return [...out, ...footer(width, keysFor(s, true), theme)];
252
+ return [...out, ...footer(width, keysFor(s, true, url), theme)];
234
253
  }
235
- function Live({ t, theme, interrupt, }) {
254
+ function Live({ t, theme, interrupt, url, }) {
236
255
  const [snap, setSnap] = useState();
237
256
  const [selected, setSelected] = useState(0);
238
257
  const [follow, setFollow] = useState();
@@ -250,6 +269,12 @@ function Live({ t, theme, interrupt, }) {
250
269
  interrupt();
251
270
  return;
252
271
  }
272
+ if (input === 'o') {
273
+ if (url) {
274
+ openInBrowser(url);
275
+ }
276
+ return;
277
+ }
253
278
  if (follow) {
254
279
  if (key.escape) {
255
280
  setFollow(undefined);
@@ -271,8 +296,8 @@ function Live({ t, theme, interrupt, }) {
271
296
  }
272
297
  const height = Math.max(8, rows - 1);
273
298
  const all = follow
274
- ? followLines(snap, follow, columns, height, theme)
275
- : mainLines(snap, at, columns, height, theme);
299
+ ? followLines(snap, follow, columns, height, theme, url)
300
+ : mainLines(snap, at, columns, height, theme, url);
276
301
  // Too short a terminal loses rows from the middle, never the keys at the bottom.
277
302
  const lines = all.length > height ? [...all.slice(0, height - 2), ...all.slice(-2)] : all;
278
303
  return (_jsx(Box, { flexDirection: "column", children: lines.map((parts, i) => (_jsx(Text, { wrap: "truncate-end", children: parts.length === 0
@@ -280,9 +305,9 @@ function Live({ t, theme, interrupt, }) {
280
305
  : parts.map((p, j) => (_jsx(Text, { color: p.color, bold: p.bold, children: p.text }, j))) }, i))) }));
281
306
  }
282
307
  /** Draws the tuning until the returned function is called. */
283
- export async function watch(t, interrupt) {
308
+ export async function watch(t, interrupt, url) {
284
309
  const theme = await resolveTheme();
285
- const instance = render(_jsx(Live, { t: t, theme: theme, interrupt: interrupt }), {
310
+ const instance = render(_jsx(Live, { t: t, theme: theme, interrupt: interrupt, url: url }), {
286
311
  stdout: process.stderr,
287
312
  exitOnCtrlC: false,
288
313
  });
@@ -5,6 +5,7 @@ import { Improvements } from './improvements.ts';
5
5
  import { Journal } from './journal.ts';
6
6
  import type { Snapshot } from './live.ts';
7
7
  import { Memories } from './memories.ts';
8
+ import { Rechecks } from './recheck.ts';
8
9
  import { SystemVersions } from './system.ts';
9
10
  import type { ByStage } from './usage.ts';
10
11
  export declare const FINETUNE_DIR = "finetune";
@@ -19,6 +20,16 @@ export interface Config {
19
20
  /** merge the submitted memories after this many more arrive */
20
21
  mergeEvery: number;
21
22
  seed: number;
23
+ /** completed cases run again after each apply, to catch what it broke */
24
+ recheck: number;
25
+ /** bytes one apply may add to what the agents always read */
26
+ budget: number;
27
+ /** percent over the start past which an apply may add nothing */
28
+ maxGrowth: number;
29
+ /** bytes one on-demand skill may hold */
30
+ skillCap: number;
31
+ /** skills one agent may see */
32
+ maxSkills: number;
22
33
  classes?: string[];
23
34
  ids?: string[];
24
35
  rubric?: boolean;
@@ -33,6 +44,11 @@ export interface Worker {
33
44
  attempt?: number;
34
45
  step: Step;
35
46
  since: number;
47
+ /** a recheck: the apply whose version it runs on, and the try's folder */
48
+ recheck?: {
49
+ apply: string;
50
+ dir: string;
51
+ };
36
52
  }
37
53
  /** A case in flight. It holds a worker only while it runs or is analyzed; parked, it holds none. */
38
54
  export interface Seat {
@@ -84,6 +100,7 @@ export declare class Tuning {
84
100
  readonly system: SystemVersions;
85
101
  readonly journal: Journal;
86
102
  readonly improvements: Improvements;
103
+ readonly rechecks: Rechecks;
87
104
  readonly memories: Memories;
88
105
  stopping: boolean;
89
106
  finished: boolean;
@@ -94,6 +111,8 @@ export declare class Tuning {
94
111
  drained: boolean;
95
112
  /** cases started by this process: a failed one is not taken twice in one start */
96
113
  readonly tried: Set<string>;
114
+ /** `--more`: cases of any kind this start may take; not kept in loop.json */
115
+ more: number | undefined;
97
116
  /** cases failed since the last step that went through */
98
117
  failures: number;
99
118
  /** what the last status page counted */
@@ -104,6 +123,8 @@ export declare class Tuning {
104
123
  } | undefined;
105
124
  /** called with every live snapshot */
106
125
  readonly watchers: Set<(s: Snapshot) => void>;
126
+ /** the latest live snapshot */
127
+ live: Snapshot | undefined;
107
128
  /** the latest step failures, newest first */
108
129
  readonly errors: StepFailure[];
109
130
  constructor(root: string, config: Config, zen: Zen, tickMs?: number);
@@ -135,6 +156,10 @@ export declare class Tuning {
135
156
  id: string;
136
157
  rev: number;
137
158
  }, phase: Phase, attempt: number): string;
159
+ /** The folder of the try a worker is on: the case's own, or a recheck's. */
160
+ workDir(w: Worker & {
161
+ case: Case;
162
+ }): string;
138
163
  /** What the loop knows of a try, which a feedback file is never trusted for. */
139
164
  known(c: {
140
165
  id: string;
@@ -159,7 +184,8 @@ export declare class Tuning {
159
184
  /** A step failed: kept for the live view, and in events.jsonl with its log. */
160
185
  error(where: string, message: string, log?: string): void;
161
186
  event(e: Omit<Event, 'at'>): void;
162
- report(): void;
187
+ /** `live`: only what moves by the second is read again; the rest comes from the last full report. */
188
+ report(live?: boolean): void;
163
189
  }
164
190
  /** The real `zen`: this same binary, in its own process group so Ctrl-C reaches only us. */
165
191
  export declare function spawnZen(root: string, children: Set<ChildProcess>): Zen;
@@ -7,6 +7,7 @@ import { forget, remember } from "./children.js";
7
7
  import { Improvements } from "./improvements.js";
8
8
  import { Journal } from "./journal.js";
9
9
  import { Memories } from "./memories.js";
10
+ import { Rechecks } from "./recheck.js";
10
11
  import { writeStatus } from "./report.js";
11
12
  import { SystemVersions } from "./system.js";
12
13
  // ---------------------------------------------------------------------------
@@ -26,6 +27,11 @@ export const DEFAULTS = {
26
27
  memTries: 3,
27
28
  mergeEvery: 3,
28
29
  seed: 1,
30
+ recheck: 3,
31
+ budget: 1500,
32
+ maxGrowth: 50,
33
+ skillCap: 12_000,
34
+ maxSkills: 10,
29
35
  };
30
36
  export class Stopped extends Error {
31
37
  constructor() {
@@ -48,6 +54,7 @@ export class Tuning {
48
54
  system;
49
55
  journal;
50
56
  improvements;
57
+ rechecks;
51
58
  memories;
52
59
  stopping = false;
53
60
  finished = false;
@@ -58,12 +65,16 @@ export class Tuning {
58
65
  drained = false;
59
66
  /** cases started by this process: a failed one is not taken twice in one start */
60
67
  tried = new Set();
68
+ /** `--more`: cases of any kind this start may take; not kept in loop.json */
69
+ more;
61
70
  /** cases failed since the last step that went through */
62
71
  failures = 0;
63
72
  /** what the last status page counted */
64
73
  progress;
65
74
  /** called with every live snapshot */
66
75
  watchers = new Set();
76
+ /** the latest live snapshot */
77
+ live;
67
78
  /** the latest step failures, newest first */
68
79
  errors = [];
69
80
  constructor(root, config, zen, tickMs = 1000) {
@@ -80,6 +91,7 @@ export class Tuning {
80
91
  this.system = new SystemVersions(root, join(this.dir, 'systems'));
81
92
  this.journal = new Journal(join(this.dir, 'journal.jsonl'));
82
93
  this.improvements = new Improvements(this);
94
+ this.rechecks = new Rechecks(this);
83
95
  this.memories = new Memories(this);
84
96
  }
85
97
  get stopFile() {
@@ -139,6 +151,10 @@ export class Tuning {
139
151
  attemptDir(c, phase, attempt) {
140
152
  return join(this.caseDir(c), `${pad(attempt)}-${phase}`);
141
153
  }
154
+ /** The folder of the try a worker is on: the case's own, or a recheck's. */
155
+ workDir(w) {
156
+ return w.recheck?.dir ?? this.attemptDir(w.case, w.phase ?? 'nomem', w.attempt ?? 1);
157
+ }
142
158
  /** What the loop knows of a try, which a feedback file is never trusted for. */
143
159
  known(c, phase, attempt) {
144
160
  const round = this.round(c);
@@ -163,6 +179,7 @@ export class Tuning {
163
179
  case: s.case,
164
180
  phase: undefined,
165
181
  attempt: undefined,
182
+ recheck: undefined,
166
183
  step: 'idle',
167
184
  since: Date.now(),
168
185
  });
@@ -188,6 +205,7 @@ export class Tuning {
188
205
  case: undefined,
189
206
  phase: undefined,
190
207
  attempt: undefined,
208
+ recheck: undefined,
191
209
  step: 'idle',
192
210
  since: Date.now(),
193
211
  });
@@ -233,9 +251,10 @@ export class Tuning {
233
251
  this.recent.length = Math.min(this.recent.length, 50);
234
252
  this.report();
235
253
  }
236
- report() {
254
+ /** `live`: only what moves by the second is read again; the rest comes from the last full report. */
255
+ report(live = false) {
237
256
  try {
238
- writeStatus(this);
257
+ writeStatus(this, live);
239
258
  }
240
259
  catch {
241
260
  // A status page that failed to render must not stop the tuning.
@@ -40,6 +40,8 @@ export interface RunMetrics {
40
40
  branches: number;
41
41
  compactions: number;
42
42
  handoffs: number;
43
+ /** skills loaded, preloads included, in the order first loaded */
44
+ skills: string[];
43
45
  /** the first error a sandbox tool got from the container engine, not from a command */
44
46
  sandbox?: string;
45
47
  }
@@ -57,6 +59,8 @@ export declare function runTokens(runDir: string): ByModel | undefined;
57
59
  export declare function envelopeTokens(file: string): ByModel | undefined;
58
60
  /** Every envelope in `dir` named `<stem>.json` or `<stem>-<n>.json` - retries keep their own. */
59
61
  export declare function envelopesIn(dir: string, stem: string): ByModel | undefined;
62
+ /** An apply's own envelopes, and - for applies from before rechecks - its guard's runs: `guard/<attempt>/<case>/<try>/`. */
63
+ export declare function applyTokens(dir: string): ByModel | undefined;
60
64
  /** 1234 → 1.2k, 2184341 → 2.2M. */
61
65
  export declare function short(n: number): string;
62
66
  //# sourceMappingURL=usage.d.ts.map
@@ -88,6 +88,7 @@ export function runMetrics(runDir) {
88
88
  branches: 0,
89
89
  compactions: 0,
90
90
  handoffs: 0,
91
+ skills: [],
91
92
  };
92
93
  const seen = new Set();
93
94
  const count = (o, ms) => {
@@ -140,6 +141,14 @@ export function runMetrics(runDir) {
140
141
  case 'handoff':
141
142
  m.handoffs++;
142
143
  return;
144
+ case 'load_skills':
145
+ for (const s of Array.isArray(o.skills) ? o.skills : []) {
146
+ const name = s?.name;
147
+ if (typeof name === 'string' && !m.skills.includes(name)) {
148
+ m.skills.push(name);
149
+ }
150
+ }
151
+ return;
143
152
  }
144
153
  };
145
154
  // A branch lane starts where its parent lane was when the fork began.
@@ -212,6 +221,32 @@ export function envelopesIn(dir, stem) {
212
221
  const parts = names.map((n) => envelopeTokens(join(dir, n))).filter((p) => !!p);
213
222
  return parts.length > 0 ? mergeModels(...parts) : undefined;
214
223
  }
224
+ /** An apply's own envelopes, and - for applies from before rechecks - its guard's runs: `guard/<attempt>/<case>/<try>/`. */
225
+ export function applyTokens(dir) {
226
+ const parts = [envelopesIn(dir, 'apply')];
227
+ const under = (d) => {
228
+ try {
229
+ return readdirSync(d, { withFileTypes: true })
230
+ .filter((e) => e.isDirectory())
231
+ .map((e) => join(d, e.name));
232
+ }
233
+ catch {
234
+ return [];
235
+ }
236
+ };
237
+ for (const tryDir of under(join(dir, 'guard')).flatMap(under).flatMap(under)) {
238
+ parts.push(envelopesIn(tryDir, 'analyze'));
239
+ try {
240
+ const run = JSON.parse(readFileSync(join(tryDir, 'run.json'), 'utf8'));
241
+ parts.push(run.tokens);
242
+ }
243
+ catch {
244
+ // A void run wrote none.
245
+ }
246
+ }
247
+ const found = parts.filter((p) => !!p);
248
+ return found.length > 0 ? mergeModels(...found) : undefined;
249
+ }
215
250
  /** 1234 → 1.2k, 2184341 → 2.2M. */
216
251
  export function short(n) {
217
252
  if (n >= 1_000_000) {
@@ -0,0 +1,9 @@
1
+ export interface Viewing {
2
+ url: string;
3
+ close(): Promise<void>;
4
+ }
5
+ /** The address another live process serves the tuning on, if one does. */
6
+ export declare function liveView(projectDir: string): string | undefined;
7
+ /** Serves the project, opening at STATUS.md, until `close`. */
8
+ export declare function startView(projectDir: string): Promise<Viewing>;
9
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { liveHolder, ownLock } from '@zenera/neo';
2
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { FINETUNE_DIR } from "./tuning.js";
5
+ const lockFile = (projectDir) => join(projectDir, FINETUNE_DIR, 'view.json');
6
+ /** The address another live process serves the tuning on, if one does. */
7
+ export function liveView(projectDir) {
8
+ return liveHolder(lockFile(projectDir))?.url;
9
+ }
10
+ /** Serves the project, opening at STATUS.md, until `close`. */
11
+ export async function startView(projectDir) {
12
+ const { serveView } = await import('@zenera/view');
13
+ const server = await serveView({ root: projectDir, start: `/${FINETUNE_DIR}/STATUS.md` });
14
+ const file = lockFile(projectDir);
15
+ mkdirSync(dirname(file), { recursive: true });
16
+ writeFileSync(file, `${JSON.stringify(ownLock({ url: server.url }), null, 2)}\n`);
17
+ return {
18
+ url: server.url,
19
+ close: async () => {
20
+ if (liveHolder(file)?.pid === process.pid) {
21
+ rmSync(file, { force: true });
22
+ }
23
+ await server.close();
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=view.js.map
@@ -4,11 +4,12 @@ export { paths, readJson, writeJson } from '../home.ts';
4
4
  export { envOf, type KeyEntry, type KeyStore, type Provider } from '../keys.ts';
5
5
  export { PROVIDER_NAMES, splitRef } from '../modelref.ts';
6
6
  export { duration } from '../narrate.ts';
7
- export { copyTree, MEMORY_RULES, TEMPLATES } from '../scaffold.ts';
7
+ export { copyTree, SHARED_RULES, TEMPLATES } from '../scaffold.ts';
8
8
  export { bold, CliError, confirm, credentialError, cut, cyan, dim, EXIT, green, invalidError, json, note, pad, plain, red, styled, table, usageError, write, writeAll, yellow, } from '../term.ts';
9
9
  export { safe } from '../trace.ts';
10
10
  export { formatInline, formatMarkdown } from '../tui/markdown.ts';
11
11
  export { resolveTheme, type Theme } from '../tui/theme.ts';
12
12
  export { boxWidth } from '../tui/wrap.ts';
13
13
  export { META_PROMPT_ENV, META_SESSION_ENV } from '../usage.ts';
14
+ export { validateProject, type Report as ProjectReport } from '../validate.ts';
14
15
  //# sourceMappingURL=host.d.ts.map