@zenera/cli 1.1.23 → 1.1.25

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 (184) hide show
  1. package/README.md +88 -2
  2. package/dist/ask.d.ts +51 -0
  3. package/dist/ask.js +166 -0
  4. package/dist/banner.js +2 -2
  5. package/dist/batch.d.ts +83 -0
  6. package/dist/batch.js +413 -0
  7. package/dist/catalog.d.ts +11 -1
  8. package/dist/catalog.js +80 -7
  9. package/dist/command.d.ts +6 -0
  10. package/dist/commands/inspect.d.ts +57 -0
  11. package/dist/commands/inspect.js +539 -91
  12. package/dist/commands/list.js +128 -19
  13. package/dist/commands/memory.js +461 -32
  14. package/dist/commands/meta.js +311 -21
  15. package/dist/commands/models.js +110 -14
  16. package/dist/commands/open.js +4 -4
  17. package/dist/commands/run.js +175 -36
  18. package/dist/commands/sandbox.js +148 -15
  19. package/dist/engine.d.ts +24 -0
  20. package/dist/engine.js +61 -8
  21. package/dist/eventlog.d.ts +59 -0
  22. package/dist/eventlog.js +146 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +20 -3
  25. package/dist/main.js +5 -2
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +60 -0
  28. package/dist/meta/finetune/children.d.ts +8 -0
  29. package/dist/meta/finetune/children.js +76 -0
  30. package/dist/meta/finetune/command.d.ts +30 -0
  31. package/dist/meta/finetune/command.js +338 -0
  32. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  33. package/dist/meta/finetune/dataset/anchors.js +239 -0
  34. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  35. package/dist/meta/finetune/dataset/changes.js +319 -0
  36. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  37. package/dist/meta/finetune/dataset/command.js +1011 -0
  38. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  39. package/dist/meta/finetune/dataset/drift.js +71 -0
  40. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  41. package/dist/meta/finetune/dataset/export.js +51 -0
  42. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  43. package/dist/meta/finetune/dataset/index.js +2 -0
  44. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  45. package/dist/meta/finetune/dataset/sample.js +197 -0
  46. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/schema.js +216 -0
  48. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  49. package/dist/meta/finetune/dataset/store.js +177 -0
  50. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  51. package/dist/meta/finetune/dataset/types.js +39 -0
  52. package/dist/meta/finetune/failure.d.ts +15 -0
  53. package/dist/meta/finetune/failure.js +26 -0
  54. package/dist/meta/finetune/feedback.d.ts +45 -0
  55. package/dist/meta/finetune/feedback.js +85 -0
  56. package/dist/meta/finetune/improvements.d.ts +51 -0
  57. package/dist/meta/finetune/improvements.js +244 -0
  58. package/dist/meta/finetune/index.d.ts +2 -0
  59. package/dist/meta/finetune/index.js +2 -0
  60. package/dist/meta/finetune/journal.d.ts +35 -0
  61. package/dist/meta/finetune/journal.js +135 -0
  62. package/dist/meta/finetune/live.d.ts +74 -0
  63. package/dist/meta/finetune/live.js +226 -0
  64. package/dist/meta/finetune/loop.d.ts +3 -0
  65. package/dist/meta/finetune/loop.js +114 -0
  66. package/dist/meta/finetune/memories.d.ts +47 -0
  67. package/dist/meta/finetune/memories.js +154 -0
  68. package/dist/meta/finetune/report.d.ts +33 -0
  69. package/dist/meta/finetune/report.js +1284 -0
  70. package/dist/meta/finetune/retry.d.ts +24 -0
  71. package/dist/meta/finetune/retry.js +96 -0
  72. package/dist/meta/finetune/sampler.d.ts +18 -0
  73. package/dist/meta/finetune/sampler.js +41 -0
  74. package/dist/meta/finetune/system.d.ts +27 -0
  75. package/dist/meta/finetune/system.js +132 -0
  76. package/dist/meta/finetune/train.d.ts +39 -0
  77. package/dist/meta/finetune/train.js +391 -0
  78. package/dist/meta/finetune/tui.d.ts +16 -0
  79. package/dist/meta/finetune/tui.js +291 -0
  80. package/dist/meta/finetune/tuning.d.ts +167 -0
  81. package/dist/meta/finetune/tuning.js +289 -0
  82. package/dist/meta/finetune/usage.d.ts +62 -0
  83. package/dist/meta/finetune/usage.js +225 -0
  84. package/dist/meta/host.d.ts +14 -0
  85. package/dist/meta/host.js +20 -0
  86. package/dist/meta/index.d.ts +11 -0
  87. package/dist/meta/index.js +11 -0
  88. package/dist/meta/launch.d.ts +81 -0
  89. package/dist/meta/launch.js +286 -0
  90. package/dist/meta/model.d.ts +44 -0
  91. package/dist/meta/model.js +119 -0
  92. package/dist/meta/prompts.d.ts +18 -0
  93. package/dist/meta/prompts.js +61 -0
  94. package/dist/meta/relay.d.ts +20 -0
  95. package/dist/meta/relay.js +123 -0
  96. package/dist/meta/tokens.d.ts +52 -0
  97. package/dist/meta/tokens.js +226 -0
  98. package/dist/meta/window.d.ts +61 -0
  99. package/dist/meta/window.js +198 -0
  100. package/dist/meta/wire.d.ts +42 -0
  101. package/dist/meta/wire.js +129 -0
  102. package/dist/modelref.d.ts +16 -0
  103. package/dist/modelref.js +41 -0
  104. package/dist/narrate.js +5 -2
  105. package/dist/podman.d.ts +42 -0
  106. package/dist/podman.js +198 -5
  107. package/dist/progress.d.ts +33 -0
  108. package/dist/progress.js +402 -0
  109. package/dist/projects.d.ts +0 -5
  110. package/dist/projects.js +2 -15
  111. package/dist/request.d.ts +45 -0
  112. package/dist/request.js +300 -0
  113. package/dist/sandbox.d.ts +2 -0
  114. package/dist/sandbox.js +20 -3
  115. package/dist/scaffold.d.ts +15 -6
  116. package/dist/scaffold.js +5 -60
  117. package/dist/session.d.ts +26 -1
  118. package/dist/session.js +46 -32
  119. package/dist/term.d.ts +5 -0
  120. package/dist/term.js +28 -3
  121. package/dist/trace.d.ts +84 -0
  122. package/dist/trace.js +737 -0
  123. package/dist/tui/models.d.ts +33 -0
  124. package/dist/tui/models.js +389 -0
  125. package/dist/tui/wrap.d.ts +5 -0
  126. package/dist/tui/wrap.js +12 -0
  127. package/dist/usage.d.ts +78 -0
  128. package/dist/usage.js +51 -0
  129. package/dist/validate.js +25 -3
  130. package/package.json +3 -2
  131. package/templates/editor/.github/copilot-instructions.md +41 -107
  132. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  133. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  134. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  135. package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
  136. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  137. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  138. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  139. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  140. package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
  141. package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
  142. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  143. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  144. package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
  145. package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
  146. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  147. package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
  148. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  149. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  150. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  151. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  152. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  153. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  154. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  155. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1791 -0
  156. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  157. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  158. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  159. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  160. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  161. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  162. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  163. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  164. package/templates/editor/.github/skills/zen-inspect/SKILL.md +431 -0
  165. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  166. package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
  167. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
  168. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  169. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  170. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  171. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
  172. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  173. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  174. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  175. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  176. package/templates/editor/.vscode/agents.schema.json +1 -1
  177. package/templates/editor/.vscode/settings.json +2 -0
  178. package/templates/project/agents/files-instructions.md +35 -13
  179. package/templates/project/agents/fork-instructions.md +27 -0
  180. package/templates/project/agents/memory-instructions.md +106 -34
  181. package/templates/project/agents/tools-instructions.md +7 -4
  182. package/templates/project/gitignore +5 -0
  183. package/dist/meta.d.ts +0 -221
  184. package/dist/meta.js +0 -684
@@ -0,0 +1,391 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { hostname } from 'node:os';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { EXIT } from "../host.js";
6
+ import { sessionKept } from "../tokens.js";
7
+ import { batchInput } from "./dataset/export.js";
8
+ import { DatasetStore } from "./dataset/store.js";
9
+ import { errorIn, sizeOf, StepError } from "./failure.js";
10
+ import { readFeedback } from "./feedback.js";
11
+ import { caseTokens, writeFeedback } from "./report.js";
12
+ import { readJson } from "./tuning.js";
13
+ import { envelopesIn, runMetrics, spent, tokensOf, } from "./usage.js";
14
+ // ---------------------------------------------------------------------------
15
+ // One case, start to finish
16
+ //
17
+ // `train` is the whole method: tries without memory until the case passes,
18
+ // then tries with the memory that passing run wrote. Every step leaves its
19
+ // result in the attempt's folder and is skipped when that result is already
20
+ // there, so calling `train` again after a stop carries on where it was.
21
+ // ---------------------------------------------------------------------------
22
+ export async function train(t, s, c) {
23
+ let passed;
24
+ for (let attempt = 1; attempt <= t.config.tries; attempt++) {
25
+ const { run, feedback } = await tryOnce(t, s, c, 'nomem', attempt);
26
+ if (feedback.done) {
27
+ passed = run;
28
+ break;
29
+ }
30
+ if (attempt < t.config.tries) {
31
+ await t.improvements.apply(feedback, s);
32
+ }
33
+ }
34
+ if (!passed) {
35
+ return markDifficult(t, c, 'nomem', 'wrong');
36
+ }
37
+ let cheaper = false;
38
+ for (let attempt = 1; attempt <= t.config.memTries; attempt++) {
39
+ const { run, feedback } = await tryOnce(t, s, c, 'mem', attempt, passed.memory);
40
+ if (feedback.done) {
41
+ cheaper = true;
42
+ t.memories.submit(c, attempt, run);
43
+ break;
44
+ }
45
+ if (attempt < t.config.memTries) {
46
+ await t.improvements.apply(feedback, s);
47
+ }
48
+ }
49
+ if (!cheaper) {
50
+ return markDifficult(t, c, 'mem', 'memory');
51
+ }
52
+ markCompleted(t, c);
53
+ }
54
+ /** A run its analysis finds void is set aside and run again: it says nothing about the prose. */
55
+ async function tryOnce(t, s, c, phase, attempt, memory) {
56
+ for (let voids = 1;; voids++) {
57
+ const run = await zenRun(t, s, c, phase, attempt, memory);
58
+ const feedback = await analyze(t, s, c, phase, attempt, run);
59
+ if (feedback.verdict !== 'void') {
60
+ return { run, feedback };
61
+ }
62
+ const dir = t.attemptDir(c, phase, attempt);
63
+ let n = 1;
64
+ while (existsSync(`${dir}.void-${n}`)) {
65
+ n++;
66
+ }
67
+ renameSync(dir, `${dir}.void-${n}`);
68
+ t.event({
69
+ what: 'set aside',
70
+ worker: s.worker?.slot,
71
+ case: c.id,
72
+ phase,
73
+ attempt,
74
+ detail: `void, kept in ${basename(dir)}.void-${n} - running the try again`,
75
+ });
76
+ if (voids >= VOIDS) {
77
+ throw new VoidRun(`${VOIDS} analyses in a row found the run void - the last: ${feedback.infra}`, join(`${dir}.void-${n}`, 'analysis.md'));
78
+ }
79
+ t.checkStopping();
80
+ }
81
+ }
82
+ export class VoidRun extends StepError {
83
+ }
84
+ /** The container engine failed: every run after this one would fail the same way. */
85
+ export class SandboxDown extends StepError {
86
+ }
87
+ /** Killed for memory, or nothing to show for it: the run says nothing about the prose. */
88
+ const VOIDS = 3;
89
+ async function zenRun(t, s, c, phase, attempt, memory) {
90
+ const dir = t.attemptDir(c, phase, attempt);
91
+ const file = join(dir, 'run.json');
92
+ for (let tries = 1; !existsSync(file); tries++) {
93
+ if (tries > VOIDS) {
94
+ throw new VoidRun(`${VOIDS} runs in a row were void`, join(dir, 'run.log'));
95
+ }
96
+ await t.improvements.notApplying();
97
+ t.at(s, phase, attempt, 'run');
98
+ t.improvements.runs++;
99
+ const began = Date.now();
100
+ try {
101
+ const why = await t.journal.step({ kind: 'run', dir }, () => runOnce(t, c, dir, memory));
102
+ const ran = why ? undefined : readJson(file);
103
+ t.ended(s, {
104
+ what: why ? 'void' : 'ran',
105
+ phase,
106
+ attempt,
107
+ result: why ?? (ran?.failed ? `failed: ${ran.failed}` : `${ran?.turns ?? '?'} turns`),
108
+ took: Date.now() - began,
109
+ tokens: spent(ran?.tokens),
110
+ });
111
+ }
112
+ finally {
113
+ t.improvements.runs--;
114
+ }
115
+ }
116
+ return readJson(file);
117
+ }
118
+ /** One `zen run`. Writes run.json, or says why the run was void. */
119
+ async function runOnce(t, c, dir, memory) {
120
+ mkdirSync(dir, { recursive: true });
121
+ const mem = join(dir, 'memory');
122
+ rmSync(mem, { recursive: true, force: true });
123
+ if (memory && existsSync(memory)) {
124
+ cpSync(memory, mem, { recursive: true, filter: (src) => basename(src) !== '.lock' });
125
+ }
126
+ // Else `zen run` roots the agent's files at its cwd: the project, shared by every run.
127
+ const workspace = join(dir, 'workspace');
128
+ rmSync(workspace, { recursive: true, force: true });
129
+ mkdirSync(workspace, { recursive: true });
130
+ const request = join(dir, 'request.json');
131
+ const envelope = join(dir, 'envelope.json');
132
+ const events = join(dir, 'events.jsonl');
133
+ rmSync(envelope, { force: true });
134
+ rmSync(events, { force: true });
135
+ writeFileSync(request, `${JSON.stringify({ input: batchInput([c], t.root, request).batch[0].input }, null, 2)}\n`);
136
+ const log = join(dir, 'run.log');
137
+ const from = existsSync(log) ? statSync(log).size : 0;
138
+ const said = () => existsSync(log) ? readFileSync(log).subarray(from).toString('utf8') : '';
139
+ const res = await t.zen([
140
+ 'run',
141
+ '--plain',
142
+ '--new',
143
+ '--yes',
144
+ '--json',
145
+ // A try is never continued: its containers would only hold podman's locks.
146
+ '--disposable',
147
+ '--input',
148
+ request,
149
+ '--memory',
150
+ mem,
151
+ '--workspace',
152
+ workspace,
153
+ '--out',
154
+ envelope,
155
+ '--events',
156
+ events,
157
+ ], log);
158
+ if (res.code === EXIT.sandbox) {
159
+ throw new SandboxDown(/error\s+(.+)/.exec(said())?.[1]?.trim() ?? 'the sandbox is not ready', log);
160
+ }
161
+ const out = readJson(envelope);
162
+ let runDir = out?.run?.dir;
163
+ let failed;
164
+ if (!runDir) {
165
+ // A failed run throws before the envelope; its directory is in the hint.
166
+ const text = said();
167
+ const report = /report: (\S+report\.html)/.exec(text)?.[1];
168
+ if (!report) {
169
+ return 'no run directory';
170
+ }
171
+ runDir = dirname(report);
172
+ failed = /error\s+(.+)/.exec(text)?.[1]?.trim() ?? 'the run failed';
173
+ }
174
+ const graph = join(runDir, 'graph.mmd');
175
+ if (existsSync(graph) && /exit code 137/.test(readFileSync(graph, 'utf8'))) {
176
+ return 'a command was killed for memory (exit 137)';
177
+ }
178
+ const meta = readJson(join(runDir, 'meta.json'));
179
+ const metrics = runMetrics(runDir);
180
+ if (metrics?.sandbox) {
181
+ throw new SandboxDown(metrics.sandbox, join(runDir, 'graph.mmd'));
182
+ }
183
+ const run = {
184
+ dir: runDir,
185
+ memory: mem,
186
+ workspace,
187
+ system: t.system.version(),
188
+ ...(existsSync(join(runDir, 'report.html')) ? { report: join(runDir, 'report.html') } : {}),
189
+ ...(existsSync(graph) ? { graph } : {}),
190
+ ...(failed || meta?.error ? { failed: failed ?? meta?.error } : {}),
191
+ ...(meta?.turns !== undefined ? { turns: meta.turns } : {}),
192
+ ...(meta?.usage ? { usage: meta.usage } : {}),
193
+ ...(metrics ? { tokens: tokensOf(metrics), metrics } : {}),
194
+ ...(meta?.durationMs !== undefined ? { durationMs: meta.durationMs } : {}),
195
+ };
196
+ writeFileSync(join(dir, 'run.json'), `${JSON.stringify(run, null, 2)}\n`);
197
+ return undefined;
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // zen meta run /analyze
201
+ // ---------------------------------------------------------------------------
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
+ }
207
+ async function analyze(t, s, c, phase, attempt, run) {
208
+ const dir = t.attemptDir(c, phase, attempt);
209
+ const file = join(dir, 'feedback.json');
210
+ const known = t.known(c, phase, attempt);
211
+ const kept = readFeedback(file, known);
212
+ let feedback;
213
+ if ('feedback' in kept) {
214
+ feedback = kept.feedback;
215
+ }
216
+ else {
217
+ t.checkStopping();
218
+ t.at(s, phase, attempt, 'analyze');
219
+ const began = Date.now();
220
+ feedback = await t.journal.step({ kind: 'analyze', dir }, () => analyzeOnce(t, s, c, phase, attempt, run, file));
221
+ const graded = Object.values(feedback.rubric);
222
+ t.ended(s, {
223
+ what: 'analyzed',
224
+ phase,
225
+ attempt,
226
+ result: feedback.verdict === 'void'
227
+ ? `void: ${feedback.infra}`
228
+ : `${feedback.verdict}${graded.length ? ` ${graded.filter((g) => g === 'pass').length}/${graded.length}` : ''}${feedback.done ? ', done' : ''}`,
229
+ took: Date.now() - began,
230
+ tokens: spent(envelopesIn(dir, 'analyze')),
231
+ });
232
+ // Only a try that went all the way through says the sandbox and the providers are up:
233
+ // a run can pass on one provider while the analysis fails on another.
234
+ if (feedback.verdict !== 'void') {
235
+ t.succeeded();
236
+ }
237
+ }
238
+ writeFeedback(t, c);
239
+ return feedback;
240
+ }
241
+ async function analyzeOnce(t, s, c, phase, attempt, run, file) {
242
+ const dir = dirname(file);
243
+ 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
+ }
250
+ const hash = t.system.hash();
251
+ const applies = t.improvements.started;
252
+ const events = join(dir, 'analyze.events.jsonl');
253
+ rmSync(events, { force: true });
254
+ const continuing = () => [
255
+ 'meta',
256
+ 'run',
257
+ '--no-refresh',
258
+ '--json',
259
+ '--events',
260
+ events,
261
+ ...(sessionKept(session) ? ['--resume', session] : ['--session-id', session]),
262
+ ];
263
+ const log = join(dir, 'analyze.log');
264
+ const from = sizeOf(log);
265
+ const res = await t.zen([
266
+ ...continuing(),
267
+ '/analyze',
268
+ run.dir,
269
+ `case=${c.id}`,
270
+ `rev=${c.rev}`,
271
+ `phase=${phase}`,
272
+ `attempt=${attempt}`,
273
+ `feedback=${file}`,
274
+ ], log);
275
+ keepAnalysis(dir, res.stdout);
276
+ // `zen meta run` starts a new session when the meta model changed since this one; follow it.
277
+ const started = sessionIn(res.stdout);
278
+ if (started && started !== session) {
279
+ session = started;
280
+ writeFileSync(join(t.caseDir(c), 'session'), `${session}\n`);
281
+ }
282
+ const rubricIds = c.rubric.map((r) => r.id);
283
+ let read = readFeedback(file, known, rubricIds);
284
+ // An agent that exited on an error would only meet it again; one that wrote a bad file can fix it.
285
+ if (!('feedback' in read) && res.code === 0) {
286
+ const again = await t.zen([
287
+ ...continuing(),
288
+ `The feedback file ${file} is ${read.problem}. Write it again exactly as section 7 of the zen-analyze-run skill describes, then stop.`,
289
+ ], log);
290
+ keepAnalysis(dir, again.stdout, false);
291
+ read = readFeedback(file, known, rubricIds);
292
+ }
293
+ // Any apply that overlapped this analysis owns the change; undoing it would gut the apply.
294
+ const overlapped = t.improvements.applying || t.improvements.started !== applies;
295
+ if (t.system.hash() !== hash && !overlapped) {
296
+ t.system.restore(t.system.version());
297
+ t.event({
298
+ what: 'analyze edited the system - undone',
299
+ worker: s.worker?.slot,
300
+ case: c.id,
301
+ phase,
302
+ attempt,
303
+ });
304
+ }
305
+ if (!('feedback' in read)) {
306
+ const e = errorIn(log, from);
307
+ throw new StepError(`analysis wrote no usable feedback (${read.problem})${e.message ? `: ${e.message}` : ''}`, e.log);
308
+ }
309
+ note(t, c, read.feedback, run, session);
310
+ return read.feedback;
311
+ }
312
+ function sessionIn(stdout) {
313
+ try {
314
+ const id = JSON.parse(stdout).sessionId;
315
+ return typeof id === 'string' && id ? id : undefined;
316
+ }
317
+ catch {
318
+ return undefined;
319
+ }
320
+ }
321
+ /** 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) {
323
+ let envelope;
324
+ try {
325
+ envelope = JSON.parse(stdout);
326
+ }
327
+ catch {
328
+ return;
329
+ }
330
+ writeFileSync(join(dir, replace ? 'analyze.json' : 'analyze-2.json'), stdout);
331
+ if (envelope.answerFile &&
332
+ existsSync(envelope.answerFile) &&
333
+ (replace || !existsSync(join(dir, 'analysis.md')))) {
334
+ copyFileSync(envelope.answerFile, join(dir, 'analysis.md'));
335
+ }
336
+ }
337
+ function note(t, c, f, run, session) {
338
+ const ids = new Set(c.rubric.map((r) => r.id));
339
+ const rubric = Object.fromEntries(Object.entries(f.rubric).filter(([id]) => ids.has(id)));
340
+ DatasetStore.open(t.root).note(c.id, {
341
+ type: 'note',
342
+ at: new Date().toISOString(),
343
+ kind: 'graded',
344
+ caseRev: c.rev,
345
+ run: run.dir,
346
+ verdict: f.verdict,
347
+ ...(Object.keys(rubric).length > 0 ? { rubric } : {}),
348
+ text: f.verdict === 'void' ? `void: ${f.infra}` : f.summary || f.verdict,
349
+ by: { session, prompt: 'analyze', host: hostname() },
350
+ });
351
+ }
352
+ // ---------------------------------------------------------------------------
353
+ // The end of a case
354
+ // ---------------------------------------------------------------------------
355
+ function finish(t, c, result) {
356
+ const full = {
357
+ ...result,
358
+ caseRev: c.rev,
359
+ at: new Date().toISOString(),
360
+ tokens: caseTokens(t, c),
361
+ };
362
+ mkdirSync(t.caseDir(c), { recursive: true });
363
+ writeFileSync(join(t.caseDir(c), 'result.json'), `${JSON.stringify(full, null, 2)}\n`);
364
+ t.event({
365
+ what: result.state,
366
+ case: c.id,
367
+ rev: c.rev,
368
+ phase: result.phase,
369
+ detail: result.reason,
370
+ });
371
+ writeFeedback(t, c);
372
+ }
373
+ export function markCompleted(t, c) {
374
+ finish(t, c, { state: 'completed', phase: 'mem' });
375
+ }
376
+ export function markDifficult(t, c, phase, reason) {
377
+ finish(t, c, { state: 'difficult', phase, reason });
378
+ DatasetStore.open(t.root).note(c.id, {
379
+ type: 'note',
380
+ at: new Date().toISOString(),
381
+ kind: 'difficult',
382
+ caseRev: c.rev,
383
+ text: `${phase === 'nomem' ? 'without memory' : 'with memory'}: ${reason}`,
384
+ by: { prompt: 'finetune', host: hostname() },
385
+ });
386
+ }
387
+ /** Something outside the prose broke. The case keeps its tries and runs again at the next start. */
388
+ export function markFailed(t, c, phase, reason) {
389
+ finish(t, c, { state: 'failed', phase, reason });
390
+ }
391
+ //# sourceMappingURL=train.js.map
@@ -0,0 +1,16 @@
1
+ import type { Theme } from '../host.ts';
2
+ import type { Snapshot } from './live.ts';
3
+ import type { Tuning } from './tuning.ts';
4
+ export interface Part {
5
+ text: string;
6
+ color?: string;
7
+ bold?: boolean;
8
+ }
9
+ export type Line = Part[];
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[];
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[];
14
+ /** Draws the tuning until the returned function is called. */
15
+ export declare function watch(t: Tuning, interrupt: () => void): Promise<() => void>;
16
+ //# sourceMappingURL=tui.d.ts.map
@@ -0,0 +1,291 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, render, Text, useInput, useWindowSize } from 'ink';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { resolveTheme } from "../host.js";
5
+ import { since } from "./report.js";
6
+ import { short } from "./usage.js";
7
+ const BAR = 20;
8
+ const fit = (text, width) => text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text.padEnd(width);
9
+ const clock = (iso) => {
10
+ const d = new Date(iso);
11
+ return Number.isNaN(d.getTime()) ? ' ' : d.toTimeString().slice(0, 8);
12
+ };
13
+ const seconds = (ms) => ms === undefined
14
+ ? ''
15
+ : ms < 60_000
16
+ ? `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`
17
+ : since(ms);
18
+ function footer(width, keys, theme) {
19
+ return [
20
+ [{ text: '─'.repeat(Math.max(0, width)), color: theme.chrome.color }],
21
+ [{ text: keys, color: theme.chrome.color }],
22
+ ];
23
+ }
24
+ function keysFor(s, following) {
25
+ if (s.state === 'stopping') {
26
+ return 'stopping: the steps under way finish first · ctrl-c again kills them';
27
+ }
28
+ if (s.state !== 'running') {
29
+ return s.state;
30
+ }
31
+ 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
+ }
35
+ function header(s, theme) {
36
+ const c = s.counts;
37
+ const n = (k) => c[k] ?? 0;
38
+ const done = n('completed') + n('difficult');
39
+ const filled = s.total ? Math.round((done / s.total) * BAR) : 0;
40
+ const quiet = theme.chrome.color;
41
+ const tally = [
42
+ { text: '█'.repeat(filled), color: theme.accent },
43
+ { text: '░'.repeat(BAR - filled), color: quiet },
44
+ { text: ` ${done} of ${s.total} done` },
45
+ ];
46
+ const add = (text, color) => {
47
+ tally.push({ text: ` · ${text}`, color });
48
+ };
49
+ if (n('completed')) {
50
+ add(`${n('completed')} completed`);
51
+ }
52
+ if (n('difficult')) {
53
+ add(`${n('difficult')} difficult`, theme.warn);
54
+ }
55
+ if (n('failed')) {
56
+ add(`${n('failed')} failed`, theme.line.error.color);
57
+ }
58
+ if (s.parked) {
59
+ add(`${s.parked} parked, apply at ${s.applyAt}`);
60
+ }
61
+ return [
62
+ [
63
+ { text: `Fine-tuning ${s.project}`, bold: true },
64
+ {
65
+ text: ` · ${s.state} · ${since(s.elapsedMs)} · system v${s.system} · ${s.workers.busy} of ${s.workers.all} workers busy`,
66
+ color: quiet,
67
+ },
68
+ ],
69
+ tally,
70
+ ];
71
+ }
72
+ const STAGE_HUE = {
73
+ dark: { run: 'green', analyze: 'magentaBright', apply: 'blueBright' },
74
+ light: { run: 'green', analyze: 'magenta', apply: 'blue' },
75
+ };
76
+ /** One hue per stage in every section: run/ran, analyze/analyzed, apply/applied. */
77
+ function hue(word, theme) {
78
+ const stage = /^(run|ran)\b/.test(word)
79
+ ? 'run'
80
+ : word.startsWith('analy')
81
+ ? 'analyze'
82
+ : word.startsWith('appl')
83
+ ? 'apply'
84
+ : undefined;
85
+ return stage && STAGE_HUE[theme.appearance][stage];
86
+ }
87
+ /** worker, task, action, elapsed, tokens, model, latest - split around the action cell. */
88
+ function nowCells(cells, width) {
89
+ const [who = '', what = '', step = '', elapsed = '', tokens = '', model = '', last = ''] = cells;
90
+ return [
91
+ `${fit(who, 9)} ${fit(what, 26)} `,
92
+ fit(step, 8),
93
+ ` ${fit(elapsed, 7)} ${fit(tokens, 6)} ${fit(model, width >= 110 ? 22 : 16)} ${last}`,
94
+ ];
95
+ }
96
+ const nowRow = (r) => [
97
+ r.who,
98
+ r.what,
99
+ r.step,
100
+ since(r.sinceMs),
101
+ r.act?.calls ? short(r.act.input + r.act.output) : '',
102
+ r.act?.model ?? '',
103
+ r.act?.last ?? '',
104
+ ];
105
+ /** case, result, took, tokens after the action: the result takes what the others leave. */
106
+ const logCells = (cells, width) => {
107
+ const [where, result, took, tokens] = cells;
108
+ return `${fit(where, 31)} ${fit(result, Math.max(10, width - 75))} ${fit(took, 7)} ${tokens}`;
109
+ };
110
+ /** The overview: under way, tokens, log - as many log rows as the height leaves. */
111
+ export function mainLines(s, selected, width, height, theme) {
112
+ const quiet = theme.chrome.color;
113
+ const out = [...header(s, theme)];
114
+ if (s.now.length > 0) {
115
+ out.push([], [{ text: 'In progress', bold: true }]);
116
+ out.push([
117
+ {
118
+ text: ` ${nowCells(['worker', 'task', 'action', 'elapsed', 'tokens', 'model', 'latest'], width).join('')}`,
119
+ color: quiet,
120
+ },
121
+ ]);
122
+ s.now.forEach((r, i) => {
123
+ const [before, step, after] = nowCells(nowRow(r), width);
124
+ out.push([
125
+ i === selected ? { text: '> ', color: theme.accent, bold: true } : { text: ' ' },
126
+ { text: before },
127
+ { text: step, color: hue(r.step, theme) },
128
+ { text: after },
129
+ ]);
130
+ });
131
+ }
132
+ if (s.tokens.length > 0) {
133
+ out.push([], [{ text: 'Tokens so far', bold: true }]);
134
+ out.push([
135
+ {
136
+ text: `${fit('stage', 8)} ${fit('model', 26)} ${'calls'.padStart(6)} ${'in'.padStart(7)} ${'out'.padStart(7)}`,
137
+ color: quiet,
138
+ },
139
+ ]);
140
+ for (const r of s.tokens) {
141
+ out.push([
142
+ { text: fit(r.stage, 8), color: hue(r.stage, theme) },
143
+ {
144
+ text: ` ${fit(r.model, 26)} ${String(r.calls).padStart(6)} ${short(r.input).padStart(7)} ${short(r.output).padStart(7)}`,
145
+ },
146
+ ]);
147
+ }
148
+ }
149
+ if (s.errors.length > 0) {
150
+ const red = theme.line.error.color;
151
+ out.push([], [{ text: 'Errors', bold: true, color: red }]);
152
+ for (const e of s.errors) {
153
+ out.push([
154
+ { text: `${clock(e.at)} `, color: quiet },
155
+ { text: `${fit(e.where, 28)} ` },
156
+ { text: e.message, color: red },
157
+ ]);
158
+ if (e.log) {
159
+ out.push([{ text: `${' '.repeat(9)}log ${e.log}`, color: quiet }]);
160
+ }
161
+ }
162
+ }
163
+ const room = height - out.length - 2 - 3;
164
+ if (s.log.length > 0 && room > 0) {
165
+ out.push([], [{ text: 'Log', bold: true }]);
166
+ out.push([
167
+ {
168
+ text: `${fit('time', 8)} ${fit('action', 18)} ${logCells(['case', 'result', 'took', 'tokens'], width)}`,
169
+ color: quiet,
170
+ },
171
+ ]);
172
+ for (const r of s.log.slice(0, room)) {
173
+ out.push([
174
+ { text: `${clock(r.at)} `, color: quiet },
175
+ { text: fit(r.action, 18), color: hue(r.action, theme) },
176
+ {
177
+ text: ` ${logCells([r.case, r.result, seconds(r.took), r.tokens ? short(r.tokens) : ''], width)}`,
178
+ },
179
+ ]);
180
+ }
181
+ }
182
+ return [...out, ...footer(width, keysFor(s, false), theme)];
183
+ }
184
+ function activity(e, theme) {
185
+ const at = { text: `${clock(e.t)} `, color: theme.chrome.color };
186
+ switch (e.type) {
187
+ case 'llm':
188
+ return [
189
+ at,
190
+ {
191
+ text: `model ${e.model} ${short(e.in)} in, ${short(e.out)} out${e.ms ? `, ${seconds(e.ms)}` : ''}`,
192
+ color: theme.chrome.color,
193
+ },
194
+ ];
195
+ case 'tool':
196
+ return e.phase === 'start'
197
+ ? [at, { text: `tool ${e.name} `, color: theme.accent }, { text: e.subject }]
198
+ : [
199
+ at,
200
+ {
201
+ text: ` ${e.ok ? 'done' : 'failed'} ${e.name}${e.ms ? ` ${seconds(e.ms)}` : ''}`,
202
+ color: e.ok ? theme.chrome.color : theme.line.error.color,
203
+ },
204
+ ];
205
+ case 'say':
206
+ return [at, { text: 'says ', color: theme.accent }, { text: e.text }];
207
+ }
208
+ }
209
+ /** One step's agent, line by line as it works. */
210
+ export function followLines(s, key, width, height, theme) {
211
+ const r = s.now.find((x) => x.key === key);
212
+ const out = [];
213
+ if (!r) {
214
+ out.push([{ text: 'Nothing is under way there now.', bold: true }]);
215
+ }
216
+ else {
217
+ out.push([
218
+ { text: `Following ${r.who}`, bold: true },
219
+ {
220
+ text: ` · ${r.what} · ${r.step} · ${since(r.sinceMs)}${r.act?.model ? ` · ${r.act.model}` : ''}${r.act?.calls ? ` · ${short(r.act.input + r.act.output)} tokens in ${r.act.calls} calls` : ''}`,
221
+ color: theme.chrome.color,
222
+ },
223
+ ]);
224
+ out.push([]);
225
+ const lines = r.act?.lines ?? [];
226
+ if (lines.length === 0) {
227
+ out.push([{ text: 'nothing yet', color: theme.chrome.color }]);
228
+ }
229
+ for (const e of lines.slice(-Math.max(1, height - out.length - 3))) {
230
+ out.push(activity(e, theme));
231
+ }
232
+ }
233
+ return [...out, ...footer(width, keysFor(s, true), theme)];
234
+ }
235
+ function Live({ t, theme, interrupt, }) {
236
+ const [snap, setSnap] = useState();
237
+ const [selected, setSelected] = useState(0);
238
+ const [follow, setFollow] = useState();
239
+ const { columns, rows } = useWindowSize();
240
+ useEffect(() => {
241
+ t.watchers.add(setSnap);
242
+ return () => {
243
+ t.watchers.delete(setSnap);
244
+ };
245
+ }, [t]);
246
+ const count = snap?.now.length ?? 0;
247
+ const at = Math.min(selected, Math.max(0, count - 1));
248
+ useInput((input, key) => {
249
+ if ((key.ctrl && input === 'c') || input === 's') {
250
+ interrupt();
251
+ return;
252
+ }
253
+ if (follow) {
254
+ if (key.escape) {
255
+ setFollow(undefined);
256
+ }
257
+ return;
258
+ }
259
+ if (key.upArrow) {
260
+ setSelected(Math.max(0, at - 1));
261
+ }
262
+ else if (key.downArrow) {
263
+ setSelected(Math.min(count - 1, at + 1));
264
+ }
265
+ else if (key.return && snap?.now[at]) {
266
+ setFollow(snap.now[at].key);
267
+ }
268
+ });
269
+ if (!snap) {
270
+ return _jsx(Text, { color: theme.chrome.color, children: "starting" });
271
+ }
272
+ const height = Math.max(8, rows - 1);
273
+ const all = follow
274
+ ? followLines(snap, follow, columns, height, theme)
275
+ : mainLines(snap, at, columns, height, theme);
276
+ // Too short a terminal loses rows from the middle, never the keys at the bottom.
277
+ const lines = all.length > height ? [...all.slice(0, height - 2), ...all.slice(-2)] : all;
278
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((parts, i) => (_jsx(Text, { wrap: "truncate-end", children: parts.length === 0
279
+ ? ' '
280
+ : parts.map((p, j) => (_jsx(Text, { color: p.color, bold: p.bold, children: p.text }, j))) }, i))) }));
281
+ }
282
+ /** Draws the tuning until the returned function is called. */
283
+ export async function watch(t, interrupt) {
284
+ const theme = await resolveTheme();
285
+ const instance = render(_jsx(Live, { t: t, theme: theme, interrupt: interrupt }), {
286
+ stdout: process.stderr,
287
+ exitOnCtrlC: false,
288
+ });
289
+ return () => instance.unmount();
290
+ }
291
+ //# sourceMappingURL=tui.js.map