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