@nodii/dev 0.6.47 → 0.6.48
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 +7 -3
- package/dist/cli.js +7 -4
- package/dist/core.js +1 -1
- package/dist/lane-supervisor.js +27 -8
- package/dist/tune.js +48 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,10 +141,10 @@ nodii-dev rules find "QUESTION" [--limit N]
|
|
|
141
141
|
nodii-dev rules show CLAUSE_ID
|
|
142
142
|
nodii-dev gather tune cases|runs
|
|
143
143
|
nodii-dev gather tune replay CASE_ID
|
|
144
|
-
nodii-dev gather tune start --cases all|ID,ID --corpus-ref REF [--trigger T] [--settings JSON]
|
|
144
|
+
nodii-dev gather tune start --cases all|ID,ID --corpus-ref REF [--pickers P,Q] [--trigger T] [--settings JSON]
|
|
145
145
|
nodii-dev gather tune exec RUN_ID [--pickers shortlist-only,gemma-e4b,gemma-12b,gemma-26b]
|
|
146
146
|
nodii-dev gather tune trial RUN_ID --case ID --picker P --iteration N [--documents A,B]
|
|
147
|
-
nodii-dev gather tune finalize RUN_ID
|
|
147
|
+
nodii-dev gather tune finalize RUN_ID [--supersedes RUN_ID --reason TEXT]
|
|
148
148
|
nodii-dev gather timings [--model M] [--last N]
|
|
149
149
|
|
|
150
150
|
nodii-dev open-questions list|show|answer
|
|
@@ -954,7 +954,11 @@ its exit code (a lane that exits 0 after Gather included). It is reported as a
|
|
|
954
954
|
`process-exit` with its code, fingerprint (`lane-unfinished-exit-0`,
|
|
955
955
|
`lane-exit-N` or `lane-signal-S`) and redacted log tail. Dev orphans that run,
|
|
956
956
|
so `lane resume` relaunches it with `recoveryOf`. A lane status Dev can't serve
|
|
957
|
-
counts as unfinished.
|
|
957
|
+
counts as unfinished. A lane that recorded BLOCKED (a blocked step, or a
|
|
958
|
+
latest heartbeat note that starts with "blocked") and then exits ends its run
|
|
959
|
+
`blocked` with that reason: never an orphan, never a death that counts toward
|
|
960
|
+
the same-death stop (hub-58). A Dev without that status gets the process exit,
|
|
961
|
+
which it also ends blocked from the lane's record. Every report carries `exit_code`, `worktree` and
|
|
958
962
|
`lane_status` evidence. `process.pid` still holds the runtime's own PID, and the
|
|
959
963
|
supervisor appends its report line to `process.log`.
|
|
960
964
|
|
package/dist/cli.js
CHANGED
|
@@ -167,13 +167,16 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
167
167
|
rules cap TOKENS # operator: set the core token cap (default 3000)
|
|
168
168
|
gather tune cases|runs # Gather tuning (AH-11): frozen cases; runs with per-picker scores
|
|
169
169
|
gather tune replay <case-id> # blind replay + trigger shortlist, for the lane-agent picker
|
|
170
|
-
gather tune start --cases all|ID,ID --corpus-ref REF [--trigger manual] [--settings JSON]
|
|
170
|
+
gather tune start --cases all|ID,ID --corpus-ref REF [--pickers P,Q] [--trigger manual] [--settings JSON]
|
|
171
|
+
# --pickers: the subset this run scores (default all five)
|
|
171
172
|
gather tune exec <run-id> [--pickers shortlist-only,gemma-e4b,gemma-12b,gemma-26b]
|
|
172
|
-
# runs each automatic picker 5x per case; a Gemma
|
|
173
|
-
# lacks is reported unavailable (exit 1), never filled in
|
|
173
|
+
# runs each automatic picker the run scores 5x per case; a Gemma
|
|
174
|
+
# model this Mac lacks is reported unavailable (exit 1), never filled in
|
|
174
175
|
gather tune trial <run-id> --case ID --picker P --iteration N [--documents A,B]
|
|
175
176
|
# one trial; lane-agent passes its own --documents pick
|
|
176
|
-
gather tune finalize <run-id>
|
|
177
|
+
gather tune finalize <run-id> [--supersedes <run-id> --reason TEXT]
|
|
178
|
+
# Dev scores the run's own picker x 5-repeat matrix; --supersedes
|
|
179
|
+
# retires an earlier run in favour of this one (its trials are kept)
|
|
177
180
|
gather picker-latency [--picker gemma-12b|gemma-e4b|gemma-26b]
|
|
178
181
|
# cold (model unloaded first) and warm pick latency, for the tuning record
|
|
179
182
|
gather timings [--model M] [--last N] # this Mac's lane Gather picks: warm-up and first-pick p50/p95
|
package/dist/core.js
CHANGED
|
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
|
|
7
|
-
export const VERSION = "0.6.
|
|
7
|
+
export const VERSION = "0.6.48";
|
|
8
8
|
export const configPath = () =>
|
|
9
9
|
process.env.NODII_DEV_CONFIG_PATH ??
|
|
10
10
|
join(homedir(), ".config/nodii-dev/config.json");
|
package/dist/lane-supervisor.js
CHANGED
|
@@ -93,10 +93,15 @@ export const doneCheckRecorded = (lane) =>
|
|
|
93
93
|
|| FINISHED_LANE.has(lane?.status);
|
|
94
94
|
|
|
95
95
|
/** A lane is BLOCKED by a blocked step (`lane step --block`) or a latest heartbeat note that says so. */
|
|
96
|
-
export
|
|
97
|
-
|
|
96
|
+
export const laneBlocked = (lane) => Boolean(laneBlockedReason(lane));
|
|
97
|
+
|
|
98
|
+
/** Why a lane is BLOCKED, or null (the same rule Dev applies to a run's exit). */
|
|
99
|
+
export function laneBlockedReason(lane) {
|
|
100
|
+
const step = (lane?.steps ?? []).find((candidate) => candidate.state === "blocked");
|
|
101
|
+
if (step) return `Lane step ${step.side}/${step.stepKey} is blocked.`;
|
|
98
102
|
const heartbeat = (lane?.events ?? []).filter((event) => event.type === "heartbeat").at(-1);
|
|
99
|
-
|
|
103
|
+
const note = String(heartbeat?.metadata?.note ?? "");
|
|
104
|
+
return /^\s*blocked\b/i.test(note) ? `Lane heartbeat: ${note.slice(0, 500)}` : null;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
/** The step a lane that stopped early goes back to (hub-58, NODII-DEV-CLI-8/9). */
|
|
@@ -114,9 +119,9 @@ export function continueTaskMessage(laneKey, lane) {
|
|
|
114
119
|
async function laneProgress(request, laneKey) {
|
|
115
120
|
try {
|
|
116
121
|
const lane = (await request("GET", `/api/v1/lanes/${encode(laneKey)}`)).data;
|
|
117
|
-
return { status: lane?.status ?? null, finished: doneCheckRecorded(lane) };
|
|
122
|
+
return { status: lane?.status ?? null, finished: doneCheckRecorded(lane), blocked: laneBlockedReason(lane) };
|
|
118
123
|
} catch {
|
|
119
|
-
return { status: null, finished: false };
|
|
124
|
+
return { status: null, finished: false, blocked: null };
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
|
|
@@ -134,7 +139,7 @@ export async function reportLaneExit({ runId, laneKey, exitCode, signal, worktre
|
|
|
134
139
|
markTerminal = markWatchedTerminal,
|
|
135
140
|
} = {}) {
|
|
136
141
|
const worktree = await inspect(worktrees);
|
|
137
|
-
const { status, finished } = await laneProgress(request, laneKey);
|
|
142
|
+
const { status, finished, blocked } = await laneProgress(request, laneKey);
|
|
138
143
|
const clean = exitCode === 0 && !signal;
|
|
139
144
|
const exitRef = signal ? `signal ${signal}` : String(exitCode ?? -1);
|
|
140
145
|
const exited = signal ? `was ended by ${signal}` : `exited with code ${exitCode ?? -1}`;
|
|
@@ -154,6 +159,11 @@ export async function reportLaneExit({ runId, laneKey, exitCode, signal, worktre
|
|
|
154
159
|
// A finished lane has nothing to resume, so a failed exit is not a death.
|
|
155
160
|
outcome = "failed";
|
|
156
161
|
body = { status: "failed", reason: `The lane process ${exited} after lane ${laneKey} reached ${status}.`, evidence };
|
|
162
|
+
} else if (blocked) {
|
|
163
|
+
// hub-58 (30 Oct): a lane that recorded BLOCKED stopped on purpose. Its
|
|
164
|
+
// run ends blocked: never an orphan, never a death to resume.
|
|
165
|
+
outcome = "blocked";
|
|
166
|
+
body = { status: "blocked", reason: `${blocked} The lane process ${exited} while BLOCKED.`, evidence };
|
|
157
167
|
} else {
|
|
158
168
|
outcome = "process_exit";
|
|
159
169
|
const logTail = redactRunText(await tail(logPath).catch(() => "")).trim();
|
|
@@ -168,8 +178,17 @@ export async function reportLaneExit({ runId, laneKey, exitCode, signal, worktre
|
|
|
168
178
|
await send(request, `/api/v1/runs/${encode(runId)}/${outcome === "process_exit" ? "process-exit" : "end"}`, body);
|
|
169
179
|
} catch (error) {
|
|
170
180
|
// The agent may have ended its own run already; that is not a failure.
|
|
171
|
-
if (error?.code
|
|
172
|
-
|
|
181
|
+
if (error?.code === "RUN_ALREADY_TERMINAL") outcome = "already_terminal";
|
|
182
|
+
// A Dev without the blocked status refuses it: report the exit, which a
|
|
183
|
+
// current Dev also ends blocked from the lane's own record.
|
|
184
|
+
else if (outcome === "blocked" && error instanceof CliError) {
|
|
185
|
+
outcome = "process_exit";
|
|
186
|
+
await send(request, `/api/v1/runs/${encode(runId)}/process-exit`, {
|
|
187
|
+
reason: body.reason,
|
|
188
|
+
deathFingerprint: signal ? `lane-signal-${signal}` : clean ? "lane-unfinished-exit-0" : `lane-exit-${exitCode ?? -1}`,
|
|
189
|
+
exitCode: Number.isInteger(exitCode) && exitCode >= 0 && exitCode <= 255 ? exitCode : -1,
|
|
190
|
+
});
|
|
191
|
+
} else throw error;
|
|
173
192
|
}
|
|
174
193
|
await markTerminal(runId).catch(() => {});
|
|
175
194
|
return { outcome, exitCode: exitCode ?? null, signal: signal ?? null, worktree: worktree.state, laneStatus: status };
|
package/dist/tune.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { CliError, devRequest, encode, query } from "./core.js";
|
|
2
2
|
import { GATHER_TIMEOUT_MS, TUNING_MODELS, gatherText, pickDocuments, warmPicker } from "./gather.js";
|
|
3
3
|
|
|
4
|
-
// Dev's tuning matrix (AH-11): every case,
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Dev's tuning matrix (AH-11): every case, each of the run's pickers, five
|
|
5
|
+
// repeats. A run names its pickers at start (all five by default; hub-58 lets an
|
|
6
|
+
// agent-picker run leave the Gemma columns out). Finalize refuses anything short
|
|
7
|
+
// of the run's own matrix, so a picker that cannot run here is reported
|
|
8
|
+
// unavailable and sends nothing — a missing trial is never faked.
|
|
7
9
|
export const TUNING_PICKERS = ["shortlist-only", "lane-agent", "gemma-e4b", "gemma-12b", "gemma-26b"];
|
|
8
10
|
export const TUNING_REPEATS = 5;
|
|
9
|
-
const AUTOMATIC = TUNING_PICKERS.filter((picker) => picker !== "lane-agent");
|
|
10
11
|
const TRIGGERS = ["setup", "trigger_change", "document_change", "missed_trigger", "manual"];
|
|
11
12
|
const SECTION_LIMIT = 3;
|
|
12
13
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -31,6 +32,15 @@ function pickerFlag(value) {
|
|
|
31
32
|
return value;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function pickersFlag(value) {
|
|
36
|
+
const pickers = list(value).map((picker) => {
|
|
37
|
+
if (!TUNING_PICKERS.includes(picker)) throw new CliError("USAGE", `--pickers must name pickers from ${TUNING_PICKERS.join(", ")}`, 2);
|
|
38
|
+
return picker;
|
|
39
|
+
});
|
|
40
|
+
if (!pickers.length || new Set(pickers).size !== pickers.length) throw new CliError("USAGE", "--pickers needs a comma list of distinct pickers", 2);
|
|
41
|
+
return pickers;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
function iterationFlag(value) {
|
|
35
45
|
const n = Number(value);
|
|
36
46
|
if (!Number.isInteger(n) || n < 1 || n > TUNING_REPEATS) throw new CliError("USAGE", `--iteration needs a whole number from 1 to ${TUNING_REPEATS}`, 2);
|
|
@@ -223,7 +233,8 @@ async function runCases(request, id) {
|
|
|
223
233
|
if (run.state !== "running") throw new CliError("TUNING_RUN_NOT_RUNNING", `tuning run ${id} is ${run.state}; only a running run accepts trials`, 1);
|
|
224
234
|
const cases = Array.isArray(run.settings?.caseIds) ? run.settings.caseIds : [];
|
|
225
235
|
if (!cases.length) throw new CliError("TUNING_RUN_INVALID", `tuning run ${id} has no frozen case cohort`, 1);
|
|
226
|
-
|
|
236
|
+
const pickers = Array.isArray(run.pickers) && run.pickers.length ? run.pickers : TUNING_PICKERS;
|
|
237
|
+
return { run, cases, pickers };
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
const putTrial = (request, runId, trial, iteration) => request("PUT", `/api/v1/gather/tuning/runs/${encode(runId)}/trials`, {
|
|
@@ -249,9 +260,12 @@ function execText(payload) {
|
|
|
249
260
|
}
|
|
250
261
|
|
|
251
262
|
function summaryText(run) {
|
|
252
|
-
const
|
|
263
|
+
const coverage = run.summary?.coverage?.note ?? run.settings?.coverage?.note;
|
|
264
|
+
const lines = [`Tuning run ${run.id}: ${run.state}${run.summary ? ` · ${run.summary.trials} trials` : ""}${coverage ? ` · ${coverage}` : ""}`];
|
|
265
|
+
if (run.state === "superseded") lines.push(` superseded by ${run.supersededByRunId}: ${run.supersededReason}`);
|
|
253
266
|
for (const [picker, s] of Object.entries(run.summary?.perPicker ?? {}))
|
|
254
267
|
lines.push(` ${picker.padEnd(15)}recall ${bps(s.recallBps)} ⚑ recall ${bps(s.flaggedRecallBps)} noise ${bps(s.noiseBps)} ~${s.averageMapTokens} tokens consistency ${bps(s.consistencyBps)}${s.flaggedMissedTrials ? ` ⚑ missed in ${s.flaggedMissedTrials} trials` : ""}`);
|
|
268
|
+
if (run.superseded) lines.push(`Superseded ${run.superseded.id}: ${run.superseded.supersededReason}`);
|
|
255
269
|
return lines.join("\n");
|
|
256
270
|
}
|
|
257
271
|
|
|
@@ -302,8 +316,16 @@ export async function tuneCommand(action, args, flags, { request = devRequest, p
|
|
|
302
316
|
catch { throw new CliError("USAGE", "--settings must be a JSON object", 2); }
|
|
303
317
|
if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new CliError("USAGE", "--settings must be a JSON object", 2);
|
|
304
318
|
}
|
|
305
|
-
const
|
|
306
|
-
|
|
319
|
+
const body = { trigger, caseIds, corpusRef: flags["corpus-ref"].trim(), settings };
|
|
320
|
+
if (flags.pickers !== undefined) body.pickers = pickersFlag(flags.pickers);
|
|
321
|
+
const payload = await request("POST", "/api/v1/gather/tuning/runs", body);
|
|
322
|
+
return { payload, human: (p) => {
|
|
323
|
+
const lines = [`Started tuning run ${p.data.id}: ${p.data.caseCount} case${p.data.caseCount === 1 ? "" : "s"} × ${p.data.pickers.length} picker${p.data.pickers.length === 1 ? "" : "s"} (${p.data.pickers.join(", ")}) × ${p.data.repeats} repeats.`];
|
|
324
|
+
if (p.data.settings?.coverage?.note) lines.push(`Coverage: ${p.data.settings.coverage.note}.`);
|
|
325
|
+
if (p.data.pickers.some((picker) => picker !== "lane-agent")) lines.push(`Run the automatic pickers: nodii-dev gather tune exec ${p.data.id}`);
|
|
326
|
+
if (p.data.pickers.includes("lane-agent")) lines.push(`Record each lane-agent pick: nodii-dev gather tune replay <case-id>, then gather tune trial ${p.data.id} --picker lane-agent`);
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
} };
|
|
307
329
|
}
|
|
308
330
|
if (action === "trial") {
|
|
309
331
|
const runId = uuid(args[0], "<run-id>");
|
|
@@ -311,8 +333,9 @@ export async function tuneCommand(action, args, flags, { request = devRequest, p
|
|
|
311
333
|
const picker = pickerFlag(flags.picker);
|
|
312
334
|
const iteration = iterationFlag(flags.iteration);
|
|
313
335
|
if (picker !== "lane-agent" && flags.documents !== undefined) throw new CliError("USAGE", "--documents is the lane-agent's own pick; other pickers choose for themselves", 2);
|
|
314
|
-
const { cases } = await runCases(request, runId);
|
|
336
|
+
const { cases, pickers } = await runCases(request, runId);
|
|
315
337
|
if (!cases.includes(caseId)) throw new CliError("TUNING_CASE_OUTSIDE_RUN", `case ${caseId} is not in run ${runId}`, 1);
|
|
338
|
+
if (!pickers.includes(picker)) throw new CliError("TUNING_TRIAL_OUTSIDE_MATRIX", `run ${runId} scores ${pickers.join(", ")}, not ${picker}`, 1);
|
|
316
339
|
const replay = (await request("GET", `/api/v1/gather/tuning/cases/${encode(caseId)}/replay`)).data;
|
|
317
340
|
if (TUNING_MODELS[picker]) await warm({ model: TUNING_MODELS[picker] });
|
|
318
341
|
const trial = await runTrial(replay, picker, { request, pick, corpus: corpus ?? tuningCorpus(request), documents: list(flags.documents) });
|
|
@@ -321,9 +344,13 @@ export async function tuneCommand(action, args, flags, { request = devRequest, p
|
|
|
321
344
|
}
|
|
322
345
|
if (action === "exec") {
|
|
323
346
|
const runId = uuid(args[0], "<run-id>");
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
326
|
-
const { cases } = await runCases(request, runId);
|
|
347
|
+
const requested = flags.pickers === undefined ? null : pickersFlag(flags.pickers);
|
|
348
|
+
if (requested?.includes("lane-agent")) throw new CliError("USAGE", "the lane-agent picker is the agent itself: record each pick with `gather tune trial --picker lane-agent`", 2);
|
|
349
|
+
const { cases, pickers: runPickers } = await runCases(request, runId);
|
|
350
|
+
const outside = (requested ?? []).filter((picker) => !runPickers.includes(picker));
|
|
351
|
+
if (outside.length) throw new CliError("TUNING_TRIAL_OUTSIDE_MATRIX", `run ${runId} scores ${runPickers.join(", ")}, not ${outside.join(", ")}`, 1);
|
|
352
|
+
// By default, every automatic picker the run scores.
|
|
353
|
+
const pickers = requested ?? runPickers.filter((picker) => picker !== "lane-agent");
|
|
327
354
|
const shared = corpus ?? tuningCorpus(request);
|
|
328
355
|
const replays = new Map();
|
|
329
356
|
const replayOf = async (caseId) => {
|
|
@@ -354,8 +381,15 @@ export async function tuneCommand(action, args, flags, { request = devRequest, p
|
|
|
354
381
|
}
|
|
355
382
|
if (action === "finalize") {
|
|
356
383
|
const runId = uuid(args[0], "<run-id>");
|
|
357
|
-
const
|
|
384
|
+
const body = {};
|
|
385
|
+
if (flags.supersedes !== undefined || flags.reason !== undefined) {
|
|
386
|
+
const supersedes = uuid(flags.supersedes, "--supersedes <run-id>");
|
|
387
|
+
if (typeof flags.reason !== "string" || !flags.reason.trim()) throw new CliError("USAGE", "--supersedes needs --reason TEXT: why the earlier run is retired", 2);
|
|
388
|
+
if (supersedes === runId) throw new CliError("USAGE", "a run cannot supersede itself", 2);
|
|
389
|
+
body.supersedes = { runId: supersedes, reason: flags.reason.trim() };
|
|
390
|
+
}
|
|
391
|
+
const payload = await request("POST", `/api/v1/gather/tuning/runs/${encode(runId)}/finalize`, body);
|
|
358
392
|
return { payload, human: (p) => summaryText(p.data) };
|
|
359
393
|
}
|
|
360
|
-
throw new CliError("USAGE", "gather tune cases|replay <case-id>|start --cases all|ID,ID --corpus-ref REF|trial <run-id> --case ID --picker P --iteration N [--documents A,B]|exec <run-id> [--pickers P,Q]|finalize <run-id
|
|
394
|
+
throw new CliError("USAGE", "gather tune cases|replay <case-id>|start --cases all|ID,ID --corpus-ref REF [--pickers P,Q]|trial <run-id> --case ID --picker P --iteration N [--documents A,B]|exec <run-id> [--pickers P,Q]|finalize <run-id> [--supersedes <run-id> --reason TEXT]|runs", 2);
|
|
361
395
|
}
|