@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.
- package/README.md +13 -1
- package/dist/catalog.d.ts +20 -0
- package/dist/catalog.js +60 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +5 -1
- package/dist/commands/inspect.js +54 -14
- package/dist/commands/memory.js +28 -3
- package/dist/commands/meta.js +24 -5
- package/dist/commands/models.js +2 -1
- package/dist/commands/sandbox.js +73 -3
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +6 -0
- package/dist/eventlog.d.ts +5 -0
- package/dist/eventlog.js +25 -1
- package/dist/liveness.js +20 -2
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +1 -1
- package/dist/meta/editor.js +4 -21
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -0
- package/dist/meta/finetune/command.d.ts +1 -1
- package/dist/meta/finetune/command.js +224 -46
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/types.d.ts +3 -1
- package/dist/meta/finetune/feedback.d.ts +14 -2
- package/dist/meta/finetune/feedback.js +22 -3
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +50 -3
- package/dist/meta/finetune/improvements.js +290 -33
- package/dist/meta/finetune/live.d.ts +17 -7
- package/dist/meta/finetune/live.js +56 -10
- package/dist/meta/finetune/loop.js +59 -10
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +9 -2
- package/dist/meta/finetune/report.js +551 -438
- package/dist/meta/finetune/sampler.js +8 -2
- package/dist/meta/finetune/train.d.ts +6 -2
- package/dist/meta/finetune/train.js +55 -21
- package/dist/meta/finetune/tui.d.ts +3 -3
- package/dist/meta/finetune/tui.js +37 -12
- package/dist/meta/finetune/tuning.d.ts +27 -1
- package/dist/meta/finetune/tuning.js +21 -2
- package/dist/meta/finetune/usage.d.ts +4 -0
- package/dist/meta/finetune/usage.js +35 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +2 -1
- package/dist/meta/host.js +2 -1
- package/dist/meta.d.ts +291 -0
- package/dist/meta.js +981 -0
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/scaffold.d.ts +3 -4
- package/dist/scaffold.js +21 -9
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +54 -5
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +3 -1
- package/dist/tui/models.js +65 -7
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +29 -43
- package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
- package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
- package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -493
- package/templates/project/agents/tools-instructions.md +7 -0
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
|
+
import { writeApplyBrief } from "./apply-brief.js";
|
|
5
|
+
import { costChanges, limits, measure, overLimits } from "./cost.js";
|
|
4
6
|
import { errorIn, sizeOf } from "./failure.js";
|
|
5
|
-
import {
|
|
7
|
+
import { forProse } from "./feedback.js";
|
|
8
|
+
import { patchFiles, writeApply } from "./report.js";
|
|
6
9
|
import { readJson, Stopped } from "./tuning.js";
|
|
7
|
-
import {
|
|
10
|
+
import { applyTokens, spent } from "./usage.js";
|
|
8
11
|
const pad = (n) => String(n).padStart(3, '0');
|
|
12
|
+
const kept = (d) => d?.decision === 'applied' || d?.decision === 'merged';
|
|
13
|
+
const DECIDED = new Set(['applied', 'merged', 'rejected', 'deferred', 'structural']);
|
|
14
|
+
/** `<request> <improvement>` for each improvement of inputs.json that decisions.json gives no valid decision. */
|
|
15
|
+
export function undecided(dir) {
|
|
16
|
+
const inputs = readJson(join(dir, 'inputs.json')) ?? [];
|
|
17
|
+
const decided = readJson(join(dir, 'decisions.json')) ?? {};
|
|
18
|
+
return inputs.flatMap((f) => forProse(f)
|
|
19
|
+
.filter((i) => !DECIDED.has(decided[f.id]?.[i.id]?.decision ?? ''))
|
|
20
|
+
.map((i) => `${f.id} ${i.id}`));
|
|
21
|
+
}
|
|
9
22
|
export class Improvements {
|
|
10
23
|
#t;
|
|
11
24
|
pending = [];
|
|
@@ -53,10 +66,85 @@ export class Improvements {
|
|
|
53
66
|
isApplied(id) {
|
|
54
67
|
return this.#applied.has(id);
|
|
55
68
|
}
|
|
56
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Every apply's decisions, later ones over earlier, per improvement. An edit
|
|
71
|
+
* that was undone kept nothing, so what it applied or merged reads `discarded`.
|
|
72
|
+
*/
|
|
73
|
+
decisions() {
|
|
74
|
+
const all = {};
|
|
75
|
+
for (const a of this.applies()) {
|
|
76
|
+
const undone = a.applied?.discarded;
|
|
77
|
+
for (const [id, byImprovement] of Object.entries(readJson(join(a.dir, 'decisions.json')) ?? {})) {
|
|
78
|
+
for (const [i, d] of Object.entries(byImprovement ?? {})) {
|
|
79
|
+
(all[id] ??= {})[i] =
|
|
80
|
+
undone && kept(d)
|
|
81
|
+
? {
|
|
82
|
+
...d,
|
|
83
|
+
decision: 'discarded',
|
|
84
|
+
note: `${d.note ?? ''} - undone: ${undone}`.trim(),
|
|
85
|
+
}
|
|
86
|
+
: d;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return all;
|
|
91
|
+
}
|
|
92
|
+
/** Improvements held back for want of a second case, and not taken up since. */
|
|
93
|
+
deferred() {
|
|
94
|
+
const open = new Map();
|
|
95
|
+
for (const a of this.applies()) {
|
|
96
|
+
const inputs = new Map((a.inputs ?? []).map((f) => [f.id, f]));
|
|
97
|
+
const undone = Boolean(a.applied?.discarded);
|
|
98
|
+
for (const [id, byImprovement] of Object.entries(readJson(join(a.dir, 'decisions.json')) ?? {})) {
|
|
99
|
+
for (const [i, d] of Object.entries(byImprovement ?? {})) {
|
|
100
|
+
const key = `${id} ${i}`;
|
|
101
|
+
if (d?.decision === 'deferred') {
|
|
102
|
+
const improvement = inputs.get(id)?.improvements.find((x) => x.id === i) ??
|
|
103
|
+
open.get(key)?.improvement;
|
|
104
|
+
if (improvement) {
|
|
105
|
+
open.set(key, {
|
|
106
|
+
request: id,
|
|
107
|
+
case: inputs.get(id)?.case ?? open.get(key)?.case ?? id.split('@')[0],
|
|
108
|
+
apply: a.name,
|
|
109
|
+
improvement,
|
|
110
|
+
...(d.note ? { note: d.note } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (!undone) {
|
|
115
|
+
open.delete(key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...open.values()];
|
|
121
|
+
}
|
|
122
|
+
/** The skills an apply made or changed for this request: the case's next run should load them. */
|
|
123
|
+
skillsFor(id) {
|
|
124
|
+
const mine = this.decisions()[id] ?? {};
|
|
125
|
+
return [
|
|
126
|
+
...new Set(Object.values(mine)
|
|
127
|
+
.filter((d) => kept(d) && d.skill)
|
|
128
|
+
.map((d) => d.skill)),
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
/** Nothing of this request was applied, merged or deferred, and some of it is structural: prose cannot fix it. */
|
|
132
|
+
#structural(id) {
|
|
133
|
+
const mine = Object.values(this.decisions()[id] ?? {});
|
|
134
|
+
const structural = mine.filter((d) => d.decision === 'structural');
|
|
135
|
+
if (structural.length === 0 ||
|
|
136
|
+
mine.some((d) => kept(d) || d.decision === 'deferred' || d.decision === 'discarded')) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return structural.map((d) => d.note || 'structural').join('; ');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parks until an apply has taken this feedback, the worker free meanwhile. Taken already - after a restart - returns at once.
|
|
143
|
+
* `structural` when the apply found nothing prose can fix.
|
|
144
|
+
*/
|
|
57
145
|
async apply(feedback, s) {
|
|
58
146
|
if (this.#applied.has(feedback.id)) {
|
|
59
|
-
return;
|
|
147
|
+
return this.#outcome(feedback.id);
|
|
60
148
|
}
|
|
61
149
|
if (!this.pending.some((f) => f.id === feedback.id)) {
|
|
62
150
|
this.pending.push(feedback);
|
|
@@ -68,7 +156,7 @@ export class Improvements {
|
|
|
68
156
|
case: feedback.case,
|
|
69
157
|
phase: feedback.phase,
|
|
70
158
|
attempt: feedback.attempt,
|
|
71
|
-
detail: `${feedback.
|
|
159
|
+
detail: `${forProse(feedback).length} improvement(s), worker freed`,
|
|
72
160
|
});
|
|
73
161
|
this.#t.unseat(s);
|
|
74
162
|
try {
|
|
@@ -82,7 +170,24 @@ export class Improvements {
|
|
|
82
170
|
finally {
|
|
83
171
|
this.parked--;
|
|
84
172
|
}
|
|
173
|
+
const outcome = this.#outcome(feedback.id);
|
|
174
|
+
if (outcome.structural) {
|
|
175
|
+
return outcome;
|
|
176
|
+
}
|
|
85
177
|
await this.#t.seat(s, true);
|
|
178
|
+
return outcome;
|
|
179
|
+
}
|
|
180
|
+
#outcome(id) {
|
|
181
|
+
const structural = this.#structural(id);
|
|
182
|
+
return structural ? { structural } : {};
|
|
183
|
+
}
|
|
184
|
+
/** A recheck's request: queued like a parked case's, with no case waiting on it. */
|
|
185
|
+
regressed(feedback) {
|
|
186
|
+
if (!this.#applied.has(feedback.id) &&
|
|
187
|
+
!this.pending.some((f) => f.id === feedback.id) &&
|
|
188
|
+
!this.inApply.some((f) => f.id === feedback.id)) {
|
|
189
|
+
this.pending.push(feedback);
|
|
190
|
+
}
|
|
86
191
|
}
|
|
87
192
|
/** Before a run starts: wait out an apply, or stop. */
|
|
88
193
|
async notApplying() {
|
|
@@ -96,7 +201,10 @@ export class Improvements {
|
|
|
96
201
|
async loop() {
|
|
97
202
|
while (!this.#t.finished) {
|
|
98
203
|
await this.#t.tick();
|
|
99
|
-
if (this.#t.stopping
|
|
204
|
+
if (this.#t.stopping) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (this.pending.length === 0) {
|
|
100
208
|
continue;
|
|
101
209
|
}
|
|
102
210
|
if (this.pending.length < this.#t.config.applyAt &&
|
|
@@ -112,6 +220,20 @@ export class Improvements {
|
|
|
112
220
|
}
|
|
113
221
|
}
|
|
114
222
|
}
|
|
223
|
+
/** What the agents read when the tuning began - its first version, even for a tuning begun before this was kept. */
|
|
224
|
+
async baseline() {
|
|
225
|
+
const file = join(this.#t.dir, 'baseline.json');
|
|
226
|
+
const saved = readJson(file)?.cost;
|
|
227
|
+
if (saved) {
|
|
228
|
+
return saved;
|
|
229
|
+
}
|
|
230
|
+
const first = this.#t.system.rows()[0];
|
|
231
|
+
const at = first ? this.#t.system.snapshotDir(first.v) : undefined;
|
|
232
|
+
const cost = await measure(at && existsSync(at) ? at : this.#t.root);
|
|
233
|
+
mkdirSync(this.#t.dir, { recursive: true });
|
|
234
|
+
writeFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), version: first?.v ?? 0, cost }, null, 2)}\n`);
|
|
235
|
+
return cost;
|
|
236
|
+
}
|
|
115
237
|
async #applyNow() {
|
|
116
238
|
const t = this.#t;
|
|
117
239
|
this.applying = true;
|
|
@@ -139,13 +261,23 @@ export class Improvements {
|
|
|
139
261
|
const t = this.#t;
|
|
140
262
|
const startedAt = new Date().toISOString();
|
|
141
263
|
mkdirSync(dir, { recursive: true });
|
|
142
|
-
writeFileSync(join(dir, 'inputs.json'), `${JSON.stringify(batch.map((f) =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
264
|
+
writeFileSync(join(dir, 'inputs.json'), `${JSON.stringify(batch.map((f) => {
|
|
265
|
+
const at = f.dir ?? t.attemptDir({ id: f.case, rev: f.caseRev }, f.phase, f.attempt);
|
|
266
|
+
const missed = readJson(join(at, 'run.json'))?.missedSkills;
|
|
267
|
+
const { dir: _, ...rest } = f;
|
|
268
|
+
return {
|
|
269
|
+
...rest,
|
|
270
|
+
improvements: forProse(f),
|
|
271
|
+
file: join(at, 'feedback.json'),
|
|
272
|
+
...(missed?.length ? { missedSkills: missed } : {}),
|
|
273
|
+
};
|
|
274
|
+
}), null, 2)}\n`);
|
|
275
|
+
writeFileSync(join(dir, 'deferred.json'), `${JSON.stringify(this.deferred(), null, 2)}\n`);
|
|
276
|
+
const total = batch.reduce((n, f) => n + forProse(f).length, 0);
|
|
147
277
|
this.current = { since: Date.now(), name, dir, requests: batch.length };
|
|
148
278
|
let to = from;
|
|
279
|
+
let discarded;
|
|
280
|
+
let bytes;
|
|
149
281
|
if (total > 0) {
|
|
150
282
|
t.event({
|
|
151
283
|
what: 'apply',
|
|
@@ -153,17 +285,20 @@ export class Improvements {
|
|
|
153
285
|
});
|
|
154
286
|
writeApply(t, dir);
|
|
155
287
|
try {
|
|
156
|
-
await this.#edit(dir, from);
|
|
288
|
+
const done = await this.#edit(dir, from);
|
|
289
|
+
bytes = done.bytes;
|
|
290
|
+
if ('discarded' in done) {
|
|
291
|
+
discarded = done.discarded;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
to = done.to;
|
|
295
|
+
}
|
|
157
296
|
}
|
|
158
297
|
catch (err) {
|
|
159
298
|
writeFileSync(join(dir, 'failed.json'), `${JSON.stringify({ at: new Date().toISOString(), startedAt, error: err.message }, null, 2)}\n`);
|
|
160
299
|
writeApply(t, dir);
|
|
161
300
|
throw err;
|
|
162
301
|
}
|
|
163
|
-
to = t.system.record('apply', name).v;
|
|
164
|
-
if (to !== from) {
|
|
165
|
-
writeFileSync(join(dir, 'diff.patch'), t.system.diff(from, to));
|
|
166
|
-
}
|
|
167
302
|
}
|
|
168
303
|
const applied = {
|
|
169
304
|
ids: batch.map((f) => f.id),
|
|
@@ -172,6 +307,8 @@ export class Improvements {
|
|
|
172
307
|
at: new Date().toISOString(),
|
|
173
308
|
startedAt,
|
|
174
309
|
...(total === 0 ? { skipped: true } : {}),
|
|
310
|
+
...(discarded ? { discarded } : {}),
|
|
311
|
+
...(bytes ? { bytes } : {}),
|
|
175
312
|
};
|
|
176
313
|
writeFileSync(join(dir, 'applied.json'), `${JSON.stringify(applied, null, 2)}\n`);
|
|
177
314
|
writeApply(t, dir);
|
|
@@ -181,22 +318,78 @@ export class Improvements {
|
|
|
181
318
|
this.count++;
|
|
182
319
|
this.current = undefined;
|
|
183
320
|
t.event({
|
|
184
|
-
what: 'applied',
|
|
185
|
-
detail: `${name}: v${from} -> v${to}, see applies/${name}/APPLY.md`,
|
|
186
|
-
result: total === 0 ? 'nothing to change' : `v${from} → v${to}`,
|
|
321
|
+
what: discarded ? 'apply undone' : 'applied',
|
|
322
|
+
detail: `${name}: ${discarded ? `undone - ${discarded}` : `v${from} -> v${to}`}, see applies/${name}/APPLY.md`,
|
|
323
|
+
result: total === 0 ? 'nothing to change' : discarded ? 'undone' : `v${from} → v${to}`,
|
|
187
324
|
took: Date.now() - Date.parse(startedAt),
|
|
188
|
-
tokens: spent(
|
|
325
|
+
tokens: spent(applyTokens(dir)),
|
|
189
326
|
});
|
|
327
|
+
if (!discarded && to !== from) {
|
|
328
|
+
t.rechecks.queue(name);
|
|
329
|
+
}
|
|
190
330
|
}
|
|
191
|
-
/**
|
|
331
|
+
/** What an apply is handed about the prose: sizes, limits, and which files grew. */
|
|
332
|
+
async #writeBudget(dir, now) {
|
|
333
|
+
const start = await this.baseline();
|
|
334
|
+
const { files: _, ...rest } = now;
|
|
335
|
+
writeFileSync(join(dir, 'budget.json'), `${JSON.stringify({
|
|
336
|
+
...rest,
|
|
337
|
+
start: { total: start.total, alwaysRead: start.alwaysRead },
|
|
338
|
+
...limits(start, this.#t.config),
|
|
339
|
+
}, null, 2)}\n`);
|
|
340
|
+
const touched = new Map();
|
|
341
|
+
for (const a of this.applies()) {
|
|
342
|
+
if (!a.applied || a.applied.discarded || !existsSync(join(a.dir, 'diff.patch'))) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
for (const f of patchFiles(readFileSync(join(a.dir, 'diff.patch'), 'utf8'))) {
|
|
346
|
+
touched.set(f.file, [...(touched.get(f.file) ?? []), a.name]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const growth = Object.keys(now.files)
|
|
350
|
+
.map((file) => ({
|
|
351
|
+
file,
|
|
352
|
+
start: start.files[file] ?? 0,
|
|
353
|
+
now: now.files[file],
|
|
354
|
+
changes: touched.get(file)?.length ?? 0,
|
|
355
|
+
recent: (touched.get(file) ?? []).slice(-5),
|
|
356
|
+
}))
|
|
357
|
+
.sort((a, b) => b.now - b.start - (a.now - a.start));
|
|
358
|
+
writeFileSync(join(dir, 'growth.json'), `${JSON.stringify(growth, null, 2)}\n`);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The meta agent edits, then two gates: `zen check` and the budget. A failed
|
|
362
|
+
* gate undoes the edit and the agent tries once more, told why. `zen check`
|
|
363
|
+
* failing twice is fatal - the agent cannot leave the project loadable; the
|
|
364
|
+
* budget failing twice only undoes it. What the edit broke elsewhere is the
|
|
365
|
+
* rechecks' to find, after it.
|
|
366
|
+
*/
|
|
192
367
|
async #edit(dir, from) {
|
|
193
368
|
const t = this.#t;
|
|
369
|
+
const name = basename(dir);
|
|
370
|
+
const before = await measure(t.root);
|
|
371
|
+
await this.#writeBudget(dir, before);
|
|
372
|
+
const bound = limits(await this.baseline(), t.config);
|
|
373
|
+
const gate = { before, limits: bound };
|
|
374
|
+
writeFileSync(join(dir, 'gate.json'), `${JSON.stringify(gate)}\n`);
|
|
375
|
+
const unchanged = { before: before.total, after: before.total };
|
|
376
|
+
const at = (step) => {
|
|
377
|
+
if (this.current) {
|
|
378
|
+
this.current.step = step;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
194
381
|
for (let attempt = 1;; attempt++) {
|
|
382
|
+
at('edit');
|
|
195
383
|
const session = randomUUID();
|
|
196
384
|
writeFileSync(join(dir, 'session'), `${session}\n`);
|
|
197
385
|
const applyLog = join(dir, 'apply.log');
|
|
198
386
|
const checkLog = join(dir, 'check.log');
|
|
199
387
|
const offset = { apply: sizeOf(applyLog), check: sizeOf(checkLog) };
|
|
388
|
+
// An undone attempt's record must not pass for this one's.
|
|
389
|
+
for (const f of ['decisions.json', 'changes.md']) {
|
|
390
|
+
rmSync(join(dir, f), { force: true });
|
|
391
|
+
}
|
|
392
|
+
const brief = await writeApplyBrief(t, dir, attempt);
|
|
200
393
|
const res = await t.zen([
|
|
201
394
|
'meta',
|
|
202
395
|
'run',
|
|
@@ -206,24 +399,88 @@ export class Improvements {
|
|
|
206
399
|
session,
|
|
207
400
|
'--events',
|
|
208
401
|
join(dir, 'apply.events.jsonl'),
|
|
402
|
+
'--prompt-file',
|
|
403
|
+
brief,
|
|
209
404
|
'/finetune-apply',
|
|
210
405
|
dir,
|
|
211
406
|
], applyLog);
|
|
212
407
|
writeFileSync(join(dir, attempt === 1 ? 'apply.json' : `apply-${attempt}.json`), res.stdout);
|
|
408
|
+
// An agent cut off by its provider can still exit 0, having decided nothing.
|
|
409
|
+
const missing = res.code === 0 ? undecided(dir) : [];
|
|
410
|
+
if (missing.length > 0) {
|
|
411
|
+
writeFileSync(join(dir, 'missing.md'), [
|
|
412
|
+
`# Attempt ${attempt} was undone: decisions missing`,
|
|
413
|
+
'',
|
|
414
|
+
'Written by the tuning loop: the apply ended without a decision for these improvements, so the project was put back. This attempt must write `decisions.json` with every one of them, and `changes.md`.',
|
|
415
|
+
'',
|
|
416
|
+
...missing.map((m) => `- ${m}`),
|
|
417
|
+
'',
|
|
418
|
+
].join('\n'));
|
|
419
|
+
t.error(`apply ${name}${attempt > 1 ? ` try ${attempt}` : ''}`, `the apply ended without a decision for ${missing.length} improvement(s)`, applyLog);
|
|
420
|
+
t.system.restore(from);
|
|
421
|
+
if (attempt === 2) {
|
|
422
|
+
throw new Error(`apply ${dir} ended twice without deciding every improvement - see missing.md`);
|
|
423
|
+
}
|
|
424
|
+
t.event({
|
|
425
|
+
what: 'apply retry',
|
|
426
|
+
detail: `no decision for ${missing.length} improvement(s)`,
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
at('check');
|
|
213
431
|
const check = await t.zen(['check', '--no-models', '--no-sandbox'], checkLog);
|
|
214
|
-
if (res.code
|
|
215
|
-
|
|
432
|
+
if (res.code !== 0 || check.code !== 0) {
|
|
433
|
+
const e = res.code !== 0
|
|
434
|
+
? errorIn(applyLog, offset.apply)
|
|
435
|
+
: errorIn(checkLog, offset.check);
|
|
436
|
+
t.error(`apply ${name}${attempt > 1 ? ` try ${attempt}` : ''}`, `${res.code !== 0 ? 'the apply failed' : 'zen check failed after the apply'}${e.message ? `: ${e.message}` : ''}`, e.log);
|
|
437
|
+
t.system.restore(from);
|
|
438
|
+
if (attempt === 2) {
|
|
439
|
+
throw new Error(`apply ${dir} left the project broken twice - see check.log`);
|
|
440
|
+
}
|
|
441
|
+
t.event({
|
|
442
|
+
what: 'apply retry',
|
|
443
|
+
detail: res.code !== 0 ? 'the apply failed' : 'zen check failed',
|
|
444
|
+
});
|
|
445
|
+
continue;
|
|
216
446
|
}
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
447
|
+
const after = await measure(t.root);
|
|
448
|
+
const over = overLimits(before, after, bound);
|
|
449
|
+
if (over.length > 0) {
|
|
450
|
+
writeFileSync(join(dir, 'budget.md'), [
|
|
451
|
+
`# Attempt ${attempt} was undone: over the limits`,
|
|
452
|
+
'',
|
|
453
|
+
'Written by the tuning loop: this edit grew what the agents read past a limit, so the project was put back. The next attempt reads this file to know what to cut.',
|
|
454
|
+
'',
|
|
455
|
+
...over.map((o) => `- ${o}`),
|
|
456
|
+
'',
|
|
457
|
+
'## What moved',
|
|
458
|
+
'',
|
|
459
|
+
...costChanges(before, after),
|
|
460
|
+
'',
|
|
461
|
+
].join('\n'));
|
|
462
|
+
t.system.restore(from);
|
|
463
|
+
if (attempt === 2) {
|
|
464
|
+
return { discarded: `over the limits: ${over[0]}`, bytes: unchanged };
|
|
465
|
+
}
|
|
466
|
+
t.event({ what: 'apply retry', detail: `over the limits: ${over[0]}` });
|
|
467
|
+
continue;
|
|
222
468
|
}
|
|
223
|
-
t.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
469
|
+
const to = t.system.record('apply', name).v;
|
|
470
|
+
if (to === from) {
|
|
471
|
+
return { to, bytes: unchanged };
|
|
472
|
+
}
|
|
473
|
+
writeFileSync(join(dir, 'diff.patch'), t.system.diff(from, to));
|
|
474
|
+
const moved = costChanges(before, after);
|
|
475
|
+
writeFileSync(join(dir, 'cost.md'), [
|
|
476
|
+
'# What moved',
|
|
477
|
+
'',
|
|
478
|
+
'Written by the tuning loop once this apply passed its gates: bytes before and after it - what each agent reads on every call, each skill body, each file under agents/.',
|
|
479
|
+
'',
|
|
480
|
+
...(moved.length > 0 ? moved : ['Nothing the agents read changed size.']),
|
|
481
|
+
'',
|
|
482
|
+
].join('\n'));
|
|
483
|
+
return { to, bytes: { before: before.total, after: after.total } };
|
|
227
484
|
}
|
|
228
485
|
}
|
|
229
486
|
/** Requests every apply so far took, for the report. */
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { EventLine } from '../host.ts';
|
|
2
2
|
import type { StepFailure, Tuning } from './tuning.ts';
|
|
3
|
-
import { type Stage } from './usage.ts';
|
|
4
|
-
export
|
|
5
|
-
calls: number;
|
|
6
|
-
input: number;
|
|
7
|
-
output: number;
|
|
8
|
-
}
|
|
3
|
+
import { type Stage, type Tokens } from './usage.ts';
|
|
4
|
+
export type ModelSpend = Tokens;
|
|
9
5
|
/** One events file, folded as it grows. */
|
|
10
6
|
export interface Activity {
|
|
11
7
|
model?: string;
|
|
@@ -13,8 +9,14 @@ export interface Activity {
|
|
|
13
9
|
input: number;
|
|
14
10
|
output: number;
|
|
15
11
|
byModel: Record<string, ModelSpend>;
|
|
16
|
-
/** the latest tool call or thing said */
|
|
12
|
+
/** the latest tool call, with what was said just before it, or the latest thing said */
|
|
17
13
|
last?: string;
|
|
14
|
+
/** said since the last tool call */
|
|
15
|
+
said?: string;
|
|
16
|
+
/** what the agent is doing now: `says: …`, `thinks: …` or `runs: …` */
|
|
17
|
+
now?: string;
|
|
18
|
+
/** the last few `now` lines, oldest first */
|
|
19
|
+
recent: string[];
|
|
18
20
|
/** the latest lines, oldest first */
|
|
19
21
|
lines: EventLine[];
|
|
20
22
|
}
|
|
@@ -27,6 +29,10 @@ export interface NowRow {
|
|
|
27
29
|
sinceMs: number;
|
|
28
30
|
stage?: Stage;
|
|
29
31
|
act?: Activity;
|
|
32
|
+
/** what it is doing now, in one line */
|
|
33
|
+
now?: string;
|
|
34
|
+
/** the last few such lines, oldest first */
|
|
35
|
+
recent?: string[];
|
|
30
36
|
}
|
|
31
37
|
export interface TokenRow extends ModelSpend {
|
|
32
38
|
stage: Stage;
|
|
@@ -61,6 +67,10 @@ export interface Snapshot {
|
|
|
61
67
|
/** the latest step failures, newest first */
|
|
62
68
|
errors: StepFailure[];
|
|
63
69
|
}
|
|
70
|
+
/** What the applier runs once the agent is done. */
|
|
71
|
+
export declare const CHECK_NOW = "runs: zen check --no-models --no-sandbox";
|
|
72
|
+
/** `now` lines a step keeps. */
|
|
73
|
+
export declare const KEEP_NOW = 6;
|
|
64
74
|
/** Reads only what each events file gained since the last look; a file cleared for a new step starts over. */
|
|
65
75
|
export declare class Tails {
|
|
66
76
|
#private;
|
|
@@ -7,7 +7,27 @@ const KEEP_ERRORS = 5;
|
|
|
7
7
|
const PHASE_WORD = { nomem: 'no memory', mem: 'with memory' };
|
|
8
8
|
/** Not log rows: a start shows in `now` and ends in an event of its own, an error has its own section. */
|
|
9
9
|
const UNLOGGED = new Set(['run', 'analyze', 'apply', 'memory merge', 'idle', 'error']);
|
|
10
|
-
|
|
10
|
+
/** Shell tools: their subject is the command itself. */
|
|
11
|
+
const SHELL = new Set(['bash', 'run_command', 'run_command_background']);
|
|
12
|
+
/** What the applier runs once the agent is done. */
|
|
13
|
+
export const CHECK_NOW = 'runs: zen check --no-models --no-sandbox';
|
|
14
|
+
/** `now` lines a step keeps. */
|
|
15
|
+
export const KEEP_NOW = 6;
|
|
16
|
+
const blank = () => ({
|
|
17
|
+
calls: 0,
|
|
18
|
+
input: 0,
|
|
19
|
+
output: 0,
|
|
20
|
+
byModel: {},
|
|
21
|
+
recent: [],
|
|
22
|
+
lines: [],
|
|
23
|
+
});
|
|
24
|
+
function doing(a, line) {
|
|
25
|
+
a.now = line;
|
|
26
|
+
a.recent.push(line);
|
|
27
|
+
if (a.recent.length > KEEP_NOW) {
|
|
28
|
+
a.recent.shift();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
11
31
|
function fold(a, text) {
|
|
12
32
|
let e;
|
|
13
33
|
try {
|
|
@@ -26,16 +46,24 @@ function fold(a, text) {
|
|
|
26
46
|
a.calls++;
|
|
27
47
|
a.input += e.in;
|
|
28
48
|
a.output += e.out;
|
|
29
|
-
const m = (a.byModel[model] ??= { calls: 0, input: 0, output: 0 });
|
|
49
|
+
const m = (a.byModel[model] ??= { calls: 0, input: 0, cached: 0, output: 0, reasoning: 0 });
|
|
30
50
|
m.calls++;
|
|
31
51
|
m.input += e.in;
|
|
52
|
+
m.cached += e.cached;
|
|
32
53
|
m.output += e.out;
|
|
54
|
+
m.reasoning += e.reasoning;
|
|
33
55
|
}
|
|
34
56
|
else if (e.type === 'tool' && e.phase === 'start') {
|
|
35
|
-
|
|
57
|
+
const tool = `${e.name} ${e.subject}`.trim();
|
|
58
|
+
a.last = a.said ? `${a.said} · ${tool}` : tool;
|
|
59
|
+
a.said = undefined;
|
|
60
|
+
doing(a, `runs: ${SHELL.has(e.name) && e.subject ? e.subject : tool}`);
|
|
36
61
|
}
|
|
37
|
-
else if (e.type === 'say') {
|
|
62
|
+
else if (e.type === 'say' || e.type === 'think') {
|
|
63
|
+
// A call's thought comes before what it said, so the words win.
|
|
64
|
+
a.said = e.text;
|
|
38
65
|
a.last = e.text;
|
|
66
|
+
doing(a, `${e.type === 'say' ? 'says' : 'thinks'}: ${e.text}`);
|
|
39
67
|
}
|
|
40
68
|
}
|
|
41
69
|
/** Reads only what each events file gained since the last look; a file cleared for a new step starts over. */
|
|
@@ -117,27 +145,34 @@ export function snapshot(t) {
|
|
|
117
145
|
if (!w.case || (w.step !== 'run' && w.step !== 'analyze')) {
|
|
118
146
|
continue;
|
|
119
147
|
}
|
|
120
|
-
const dir = t.
|
|
148
|
+
const dir = t.workDir({ ...w, case: w.case });
|
|
149
|
+
const act = watch(join(dir, w.step === 'run' ? 'events.jsonl' : 'analyze.events.jsonl'));
|
|
121
150
|
now.push({
|
|
122
151
|
key: `w${w.slot}`,
|
|
123
152
|
who: `worker ${w.slot}`,
|
|
124
153
|
step: w.step,
|
|
125
|
-
what: `${w.case.id} · ${PHASE_WORD[w.phase ?? 'nomem']} try ${w.attempt ?? 1}`,
|
|
154
|
+
what: `${w.case.id} · ${w.recheck ? 'recheck' : PHASE_WORD[w.phase ?? 'nomem']} try ${w.attempt ?? 1}`,
|
|
126
155
|
sinceMs: clock - w.since,
|
|
127
156
|
stage: w.step,
|
|
128
|
-
act
|
|
157
|
+
act,
|
|
158
|
+
...(act.now ? { now: act.now, recent: [...act.recent] } : {}),
|
|
129
159
|
});
|
|
130
160
|
}
|
|
131
161
|
const q = t.improvements;
|
|
132
162
|
if (q.current?.dir) {
|
|
163
|
+
const checking = q.current.step === 'check';
|
|
164
|
+
const act = watch(join(q.current.dir, 'apply.events.jsonl'));
|
|
165
|
+
const line = checking ? CHECK_NOW : act.now;
|
|
166
|
+
const recent = checking ? [...act.recent, CHECK_NOW].slice(-KEEP_NOW) : [...act.recent];
|
|
133
167
|
now.push({
|
|
134
168
|
key: 'apply',
|
|
135
169
|
who: 'applier',
|
|
136
|
-
step: 'apply',
|
|
170
|
+
step: checking ? 'check' : 'apply',
|
|
137
171
|
what: `apply ${basename(q.current.dir)} · ${q.current.requests} request(s)`,
|
|
138
172
|
sinceMs: clock - q.current.since,
|
|
139
173
|
stage: 'apply',
|
|
140
|
-
act
|
|
174
|
+
act,
|
|
175
|
+
...(line ? { now: line, recent } : {}),
|
|
141
176
|
});
|
|
142
177
|
}
|
|
143
178
|
else if (q.current) {
|
|
@@ -163,10 +198,20 @@ export function snapshot(t) {
|
|
|
163
198
|
const sums = new Map();
|
|
164
199
|
const add = (stage, model, s) => {
|
|
165
200
|
const key = `${stage}\u0000${model}`;
|
|
166
|
-
const row = sums.get(key) ?? {
|
|
201
|
+
const row = sums.get(key) ?? {
|
|
202
|
+
stage,
|
|
203
|
+
model,
|
|
204
|
+
calls: 0,
|
|
205
|
+
input: 0,
|
|
206
|
+
cached: 0,
|
|
207
|
+
output: 0,
|
|
208
|
+
reasoning: 0,
|
|
209
|
+
};
|
|
167
210
|
row.calls += s.calls;
|
|
168
211
|
row.input += s.input;
|
|
212
|
+
row.cached += s.cached;
|
|
169
213
|
row.output += s.output;
|
|
214
|
+
row.reasoning += s.reasoning;
|
|
170
215
|
sums.set(key, row);
|
|
171
216
|
};
|
|
172
217
|
for (const stage of STAGES) {
|
|
@@ -218,6 +263,7 @@ export function publish(t) {
|
|
|
218
263
|
// A live view that failed to draw must not stop the tuning.
|
|
219
264
|
return undefined;
|
|
220
265
|
}
|
|
266
|
+
t.live = snap;
|
|
221
267
|
for (const watcher of t.watchers) {
|
|
222
268
|
watcher(snap);
|
|
223
269
|
}
|