@nodii/dev 0.6.47 → 0.6.49

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 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. Every report carries `exit_code`, `worktree` and
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 model this Mac
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> # Dev scores the full 5-picker x 5-repeat matrix
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.47";
7
+ export const VERSION = "0.6.49";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
package/dist/doctor.js CHANGED
@@ -5,7 +5,7 @@ import { devRequest, encode, isOAuthConfig, query, setAuthProvider } from "./cor
5
5
  import { OAuthAuthProvider, decodeClaims, loadRefreshToken } from "./oauth.js";
6
6
  import { inferGitContext, repoName } from "./lanes.js";
7
7
  import { figmaFileKey, figmaLockCheck, holdsSet, notRegisteredHint, scopeText } from "./figma.js";
8
- import { PICKER_MODEL, PICKER_MODEL_DIGEST, STALE_PIN_HINT, inspectOllama } from "./gather.js";
8
+ import { PICKER_CONTEXT, PICKER_MODEL, PICKER_MODEL_DIGEST, STALE_PIN_HINT, inspectOllama } from "./gather.js";
9
9
  import { hookDiagnostics } from "./hook-doctor.js";
10
10
 
11
11
  /**
@@ -275,9 +275,16 @@ export async function doctorCommand(deps = {}) {
275
275
  }));
276
276
  else if (!picker.modelInstalled) checks.push(missing("picker_model", `${PICKER_MODEL} is not installed`, "nodii-dev setup --yes", { model: PICKER_MODEL }));
277
277
  else checks.push(missing("picker_model", `${PICKER_MODEL} digest is ${picker.digest ?? "unknown"}; this CLI pins ${PICKER_MODEL_DIGEST}; ${STALE_PIN_HINT}`, "nodii-dev setup --yes", { model: PICKER_MODEL, digest: picker.digest ?? null, requiredDigest: PICKER_MODEL_DIGEST }));
278
+ // Gather loads the picker at an explicit num_ctx; a copy resident at another
279
+ // context (Ollama's default is the model's full window) holds far more memory.
280
+ const context = picker.loadedContext ?? null;
281
+ if (context === null) checks.push(ok("picker_context", `${PICKER_MODEL} not loaded; Gather loads it with num_ctx ${PICKER_CONTEXT}`, { model: PICKER_MODEL, context: null, requiredContext: PICKER_CONTEXT }));
282
+ else if (context === PICKER_CONTEXT) checks.push(ok("picker_context", `${PICKER_MODEL} loaded with num_ctx ${PICKER_CONTEXT}`, { model: PICKER_MODEL, context, requiredContext: PICKER_CONTEXT }));
283
+ else checks.push(missing("picker_context", `${PICKER_MODEL} is loaded with context ${context}, not ${PICKER_CONTEXT}; the next lane gather reloads it`, `ollama stop ${PICKER_MODEL}`, { model: PICKER_MODEL, context, requiredContext: PICKER_CONTEXT }));
278
284
  } catch (error) {
279
285
  checks.push(missing("ollama", error?.message ?? "Ollama is unavailable", "nodii-dev setup --yes", { code: error?.code ?? null }));
280
286
  checks.push(cannot("picker_model", `needs local Ollama; expected ${PICKER_MODEL}`, { model: PICKER_MODEL }));
287
+ checks.push(cannot("picker_context", `needs local Ollama; expected num_ctx ${PICKER_CONTEXT}`, { model: PICKER_MODEL, requiredContext: PICKER_CONTEXT }));
281
288
  }
282
289
 
283
290
  // 10. Runtime hooks: present, accepted by that runtime, and observed firing.
package/dist/gather.js CHANGED
@@ -113,12 +113,14 @@ const bareDigest = (digest) => typeof digest === "string" ? digest.replace(/^sha
113
113
 
114
114
  export async function inspectOllama(options = {}) {
115
115
  const pin = options.model ?? PINNED;
116
- const [version, tags] = await Promise.all([
116
+ const [version, tags, ps] = await Promise.all([
117
117
  jsonRequest("/api/version", options),
118
118
  jsonRequest("/api/tags", options),
119
+ jsonRequest("/api/ps", options),
119
120
  ]);
120
121
  const model = (tags.models ?? []).find((x) => x?.name === pin.name || x?.model === pin.name) ?? null;
121
122
  return {
123
+ loadedContext: residentContext(ps, pin.name),
122
124
  available: true,
123
125
  version: String(version.version ?? "unknown"),
124
126
  model: pin.name,
@@ -148,8 +150,12 @@ export async function requirePicker(options = {}) {
148
150
  */
149
151
  export async function warmPicker(options = {}) {
150
152
  const status = await requirePicker(options);
153
+ const model = (options.model ?? PINNED).name;
154
+ // Ollama keeps the context a model was loaded with; a later num_ctx does not
155
+ // reload it (hub-58: resident at 262144). Unload a wrong-context copy first.
156
+ if (status.loadedContext !== null && status.loadedContext !== PICKER_CONTEXT) await unloadPicker(model, options);
151
157
  const started = Date.now();
152
- const payload = await jsonRequest("/api/generate", { ...options, timeoutMs: options.timeoutMs ?? PICKER_WARM_TIMEOUT_MS, body: { model: (options.model ?? PINNED).name, prompt: "", stream: false } });
158
+ const payload = await jsonRequest("/api/generate", { ...options, timeoutMs: options.timeoutMs ?? PICKER_WARM_TIMEOUT_MS, body: { model, prompt: "", stream: false, options: { num_ctx: PICKER_CONTEXT } } });
153
159
  return { ...status, latency: pickLatency(payload, Date.now() - started) };
154
160
  }
155
161
 
@@ -506,6 +512,12 @@ export async function modelLoaded(model, options = {}) {
506
512
  return (ps.models ?? []).some((x) => x?.name === model || x?.model === model);
507
513
  }
508
514
 
515
+ /** The context length Ollama holds the model at (/api/ps), or null when it is not resident. */
516
+ function residentContext(ps, model) {
517
+ const loaded = (ps.models ?? []).find((x) => x?.name === model || x?.model === model);
518
+ return loaded ? (loaded.context_length ?? 0) : null;
519
+ }
520
+
509
521
  const tags = (clause) => {
510
522
  const repos = clause.repos ?? [];
511
523
  const steps = clause.steps ?? [];
@@ -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 function laneBlocked(lane) {
97
- if ((lane?.steps ?? []).some((step) => step.state === "blocked")) return true;
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
- return /^\s*blocked\b/i.test(String(heartbeat?.metadata?.note ?? ""));
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 !== "RUN_ALREADY_TERMINAL") throw error;
172
- outcome = "already_terminal";
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, every picker, five repeats. Finalize
5
- // refuses anything short of the full matrix, so a picker that cannot run here is
6
- // reported unavailable and sends nothing — a missing trial is never faked.
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
- return { run, cases };
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 lines = [`Tuning run ${run.id}: ${run.state}${run.summary ? ` · ${run.summary.trials} trials` : ""}`];
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 payload = await request("POST", "/api/v1/gather/tuning/runs", { trigger, caseIds, corpusRef: flags["corpus-ref"].trim(), settings });
306
- return { payload, human: (p) => `Started tuning run ${p.data.id}: ${p.data.caseCount} case${p.data.caseCount === 1 ? "" : "s"} × ${p.data.pickers.length} pickers × ${p.data.repeats} repeats.\nRun the automatic pickers: nodii-dev gather tune exec ${p.data.id}` };
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 pickers = flags.pickers === undefined ? AUTOMATIC : list(flags.pickers).map(pickerFlag);
325
- if (pickers.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);
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 payload = await request("POST", `/api/v1/gather/tuning/runs/${encode(runId)}/finalize`, {});
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>|runs", 2);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.47",
3
+ "version": "0.6.49",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {