@nodii/dev 0.6.32 → 0.6.33

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/dist/cli.js CHANGED
@@ -178,6 +178,10 @@ Usage: nodii-dev <command> [options] [--json]
178
178
  lane amend <key> --objective|--scope|--rules TEXT --reason TEXT
179
179
  lane gather [<key>] [--delta] [--objective TEXT] [--done-check TEXT] [--components a,b] [--paths a,b]
180
180
  # infers a launched worktree; explicit contract inputs support manual lane start
181
+ lane gather [<key>] --picker agent [--shortlist ID --documents CODE[,CODE]]
182
+ # the lane agent picks instead of local Gemma: alone it prints the
183
+ # recorded trigger shortlist; with --shortlist/--documents it records
184
+ # the pick (picker: agent) and the Clause map
181
185
  run [--kind build|test_run|subagent|deploy] [--name TEXT] -- <command> [args...]
182
186
  # watched command: registers with NODII_DEV_RUN_ID,
183
187
  # heartbeats, and records exit code + redacted log tail
@@ -185,6 +189,8 @@ Usage: nodii-dev <command> [options] [--json]
185
189
  lane start <key> --title TEXT --kind KIND --side SIDE [--repo REPO] [--issue ISSUE]
186
190
  lane step <key> --side SIDE --start|--done|--block <step> [--reason TEXT]
187
191
  # --block --reason on a blocked step records its new reason
192
+ lane step <key> --side SIDE --not-applicable gather --reason-code pre_harness_lane [--note TEXT]
193
+ # coordinator only: Gather never applied (pre-Gather lane); never "done"
188
194
  lane evidence <key> --pr|--commit|--figma|--deploy|--dev-check REF
189
195
  lane subagent-quota <key> <n> # coordinator assigns 0-100 subagent slots
190
196
  lane heartbeat|resume|land|verify|prioritize|deprioritize|stop <key>
@@ -609,6 +615,8 @@ export async function main(argv = process.argv.slice(2)) {
609
615
  if (command === "lane" && action === "amend") return emitCommand(await amendLaneCommand(rest[0], f));
610
616
  if (command === "lane" && action === "gather") {
611
617
  const result = await gatherLaneCommand(rest[0], f);
618
+ // An agent picker's shortlist step records nothing yet.
619
+ if (result.payload?.data?.recorded === false) return emitCommand(result);
612
620
  // What a gather covered is the lane's gathered set for the nudge hooks.
613
621
  const asked = (value) => (typeof value === "string" ? value.split(",").map((item) => item.trim()).filter(Boolean) : []);
614
622
  await (async () => recordGathered(process.cwd(), { guidelines: asked(f.components), deltaGathered: f.delta ? (f.paths ? asked(f.paths) : await changedPaths(process.cwd())) : [] }))().catch(() => {});
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.32";
7
+ export const VERSION = "0.6.33";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
package/dist/lanes.js CHANGED
@@ -4,7 +4,7 @@ import { promisify } from "node:util";
4
4
  import { CliError, encode, devRequest, query, readConfig, requireFlag } from "./core.js";
5
5
  import { launchRecovery } from "./runtime-launcher.js";
6
6
  import { recordReviewReceipt, reviewCommitContext } from "./safety.js";
7
- import { GATHER_TIMEOUT_MS, gatherText, pickDocuments } from "./gather.js";
7
+ import { GATHER_TIMEOUT_MS, PICKER_MODEL, gatherText, pickDocuments } from "./gather.js";
8
8
  import { storeGatherFreeze } from "./freeze.js";
9
9
 
10
10
  const execFileAsync = promisify(execFile);
@@ -46,7 +46,65 @@ export async function inferGitContext() {
46
46
  }
47
47
 
48
48
  const laneLine = (x) => `${x.key}\t${x.status}\t${x.kind}/${x.side}\t${x.title}${x.stuck ? "\tSTUCK" : ""}`;
49
- const detailText = (x) => [laneLine(x), ...(x.steps ?? []).map((s) => ` ${s.side}\t${s.state}\t${s.stepKey}\t${s.label}`)].join("\n");
49
+ // GATHER-NA: a step the coordinator ruled not applicable is never shown as done.
50
+ const stepState = (s) => s.state === "not_applicable" ? `not applicable: ${s.notApplicableReason ?? "(no reason recorded)"}` : s.state;
51
+ const detailText = (x) => [laneLine(x), ...(x.steps ?? []).map((s) => ` ${s.side}\t${stepState(s)}\t${s.stepKey}\t${s.label}`)].join("\n");
52
+
53
+ /**
54
+ * GATHER-NA: `--picker agent` makes the lane agent Gather's picker. Without
55
+ * --shortlist it only shortlists and prints the triggers to pick from; with
56
+ * --shortlist ID --documents A,B it records that pick against the recorded
57
+ * shortlist: Dev reuses its objective, paths and components, refuses a pick
58
+ * it did not offer, and adds its forced triggers. Gather never records a
59
+ * pick with no shortlist behind it.
60
+ */
61
+ function agentPickerFlags(flags) {
62
+ if (flags.picker === undefined) {
63
+ if (flags.shortlist !== undefined || flags.documents !== undefined)
64
+ throw new CliError("USAGE", "--shortlist and --documents go with --picker agent.", 2);
65
+ return null;
66
+ }
67
+ if (flags.picker !== "agent")
68
+ throw new CliError("USAGE", `--picker accepts only agent; without it the local ${PICKER_MODEL} picks.`, 2);
69
+ const shortlist = typeof flags.shortlist === "string" ? flags.shortlist.trim() : undefined;
70
+ const documents = typeof flags.documents === "string" ? list(flags.documents) : undefined;
71
+ if ((flags.shortlist !== undefined && !shortlist) || (flags.documents !== undefined && !documents?.length))
72
+ throw new CliError("USAGE", "--shortlist needs an id and --documents needs one or more codes.", 2);
73
+ if (Boolean(shortlist) !== Boolean(documents))
74
+ throw new CliError("USAGE", "Record an agent pick with both --shortlist ID and --documents CODE[,CODE]; run --picker agent alone first to see the shortlist.", 2);
75
+ return { shortlist, documents };
76
+ }
77
+
78
+ // hub-58: the reason is a fixed code (Dev stores its canonical text); a note may add context.
79
+ export const NOT_APPLICABLE_REASON_CODES = ["pre_harness_lane"];
80
+ function notApplicableRuling(flags) {
81
+ const code = flags["reason-code"];
82
+ if (!NOT_APPLICABLE_REASON_CODES.includes(code))
83
+ throw new CliError("USAGE", `--not-applicable needs --reason-code ${NOT_APPLICABLE_REASON_CODES.join("|")}.`, 2);
84
+ if (flags.note !== undefined && (typeof flags.note !== "string" || !flags.note.trim()))
85
+ throw new CliError("USAGE", "--note needs non-empty text.", 2);
86
+ return { reasonCode: code, ...(flags.note !== undefined ? { note: flags.note.trim() } : {}) };
87
+ }
88
+
89
+ const shellQuote = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
90
+ // The follow-up repeats the flags that shaped this Gather, so it records the same one.
91
+ const agentFollowUp = (ref, shortlistId, flags) => [
92
+ `nodii-dev lane gather ${ref} --picker agent --shortlist ${shortlistId} --documents CODE[,CODE]`,
93
+ ...(flags.delta ? ["--delta"] : []),
94
+ // --paths/--components are not resent to Dev (it reuses the shortlist's),
95
+ // but the local gathered set for the nudge hooks reads them.
96
+ ...["objective", "done-check", "section-limit-per-document", "paths", "components"]
97
+ .filter((flag) => typeof flags[flag] === "string")
98
+ .map((flag) => `--${flag} ${shellQuote(flags[flag])}`),
99
+ ].join(" ");
100
+ const agentShortlistText = (ref, data, flags) => [
101
+ `Trigger shortlist ${data.shortlistId} · ${JSON.stringify(data.objective)}`,
102
+ "You are Gather's picker (picker: agent). Nothing is recorded yet.",
103
+ ...data.triggers.map((trigger) => ` ${trigger.code}\t${trigger.title ?? ""}: ${trigger.sentence ?? ""}${trigger.forcedBy?.length ? ` [forced by ${trigger.forcedBy.join(", ")}]` : ""}`),
104
+ "",
105
+ "Pick every document that applies (forced ones are added for you), then record the pick:",
106
+ ` ${agentFollowUp(ref, data.shortlistId, flags)}`,
107
+ ].join("\n");
50
108
 
51
109
  function evidence(flags) {
52
110
  const selected = ["pr", "commit", "figma", "deploy", "dev-check"].filter((x) => flags[x] !== undefined);
@@ -183,6 +241,7 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
183
241
  const deadline = controller ? setTimeout(() => controller.abort(), gatherTimeoutMs) : null;
184
242
  const signal = gatherSignal ?? controller.signal;
185
243
  try {
244
+ const byAgent = agentPickerFlags(flags);
186
245
  const lane = (await request("GET", `/api/v1/lanes/${encode(id)}`, undefined, { signal })).data;
187
246
  if (flags.objective !== undefined && (typeof flags.objective !== "string" || !flags.objective.trim()))
188
247
  throw new CliError("USAGE", "lane gather --objective needs non-empty text", 2);
@@ -255,9 +314,15 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
255
314
  components,
256
315
  ...(limit !== undefined ? { limit } : {}),
257
316
  };
258
- const shortlist = await request("POST", "/api/v1/rules/shortlist", shortlistBody, { signal });
317
+ // An agent pick records against the shortlist it was made from.
318
+ const shortlist = byAgent?.shortlist
319
+ ? { data: { shortlistId: byAgent.shortlist, triggers: [] } }
320
+ : await request("POST", "/api/v1/rules/shortlist", shortlistBody, { signal });
321
+ const shortlistId = shortlist.data?.shortlistId;
259
322
  const triggers = shortlist.data?.triggers ?? [];
260
- const picked = await picker({
323
+ if (byAgent && !byAgent.shortlist)
324
+ return { payload: { data: { recorded: false, picker: "agent", shortlistId, objective, triggers } }, human: (p) => agentShortlistText(ref, p.data, flags) };
325
+ const picked = byAgent ? { documents: byAgent.documents } : await picker({
261
326
  objective,
262
327
  files: paths,
263
328
  triggers: triggers.map((trigger) => ({
@@ -269,10 +334,12 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
269
334
  const payload = await request("POST", `/api/v1/lanes/${encode(id)}/gather`, {
270
335
  objective,
271
336
  documentCodes: picked.documents,
272
- paths,
273
- components,
337
+ // An agent pick answers the shortlist's own paths and components.
338
+ ...(byAgent ? {} : { paths, components }),
274
339
  mode: flags.delta ? "delta" : "full",
275
340
  ...(sectionLimit !== undefined ? { sectionLimitPerDocument: sectionLimit } : {}),
341
+ shortlistId,
342
+ picker: byAgent ? "agent" : picked.model ?? PICKER_MODEL,
276
343
  }, { signal });
277
344
  const figma = lane.freeze?.boundaries?.figma ?? (lane.launch?.figmaScopes ?? []).map((file) => ({
278
345
  fileKey: file.fileKey,
@@ -305,15 +372,24 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
305
372
  }
306
373
  }
307
374
  if (action === "step") {
308
- const selected = ["start", "done", "block"].filter((x) => flags[x]);
309
- if (selected.length !== 1) throw new CliError("USAGE", "lane step needs exactly one of --start, --done, or --block.", 2);
375
+ const selected = ["start", "done", "block", "not-applicable"].filter((x) => flags[x]);
376
+ if (selected.length !== 1) throw new CliError("USAGE", "lane step needs exactly one of --start, --done, --block, or --not-applicable.", 2);
310
377
  const side = requireFlag(flags, "side");
311
- const stepRef = need(flags[selected[0]] === true ? args[1] : flags[selected[0]], "lane step needs <key> --side <side> --start|--done|--block <step>");
378
+ const stepRef = need(flags[selected[0]] === true ? args[1] : flags[selected[0]], "lane step needs <key> --side <side> --start|--done|--block|--not-applicable <step>");
312
379
  const reason = typeof flags.reason === "string" ? flags.reason.trim() : "";
313
380
  if (flags.reason !== undefined && (selected[0] !== "block" || !reason)) throw new CliError("USAGE", "--reason TEXT goes with --block.", 2);
381
+ const ruling = selected[0] === "not-applicable" ? notApplicableRuling(flags) : null;
382
+ if (!ruling && (flags["reason-code"] !== undefined || flags.note !== undefined)) throw new CliError("USAGE", "--reason-code and --note go with --not-applicable.", 2);
314
383
  const lane = (await request("GET", `/api/v1/lanes/${encode(id)}`)).data;
315
384
  const step = lane.steps.find((x) => x.side === side && (x.id === stepRef || x.stepKey === stepRef));
316
385
  if (!step) throw new CliError("STEP_NOT_FOUND", `No ${side} step ${stepRef} on ${ref}.`, 1);
386
+ // hub-58: Gather is the only step a coordinator may rule not applicable.
387
+ if (ruling && step.stepKey !== "gather") throw new CliError("USAGE", "--not-applicable covers only the gather step.", 2);
388
+ // GATHER-NA: coordinator only; Dev refuses a lane identity (COORDINATOR_REQUIRED).
389
+ if (ruling) {
390
+ const payload = await request("POST", `/api/v1/lanes/${encode(id)}/steps/${encode(step.id)}/not-applicable`, { ...ruling, ...via });
391
+ return { payload, human: (p) => `${ref}: ${side}/${step.stepKey} not applicable: ${p.data.notApplicableReason}${ruling.note ? ` (${ruling.note})` : ""}` };
392
+ }
317
393
  // Dev's blockLaneStep stores no reason and refuses a step that is already
318
394
  // blocked (INVALID_STEP_STATE), so the reason is a lane heartbeat note
319
395
  // until Dev issue 125 lets the step carry it.
package/dist/starting.js CHANGED
@@ -396,6 +396,9 @@ export async function amendmentGuard(argv, { request = devRequest, marker = curr
396
396
 
397
397
  const runGather = (key, flags, { request, signal }) => laneCommand("gather", [key], flags, { request, gatherSignal: signal });
398
398
 
399
+ // Gather's human text is its Clause map (a function of the payload).
400
+ const gatheredText = (gathered) => typeof gathered.human === "function" ? gathered.human(gathered.payload) : gathered.human ?? "Gather complete.";
401
+
399
402
  export async function gatherLaneCommand(key, flags, { request = devRequest, marker = currentMarker, gather = runGather, acknowledge = nextCommand, gatherTimeoutMs = GATHER_TIMEOUT_MS } = {}) {
400
403
  const controller = new AbortController();
401
404
  const deadline = setTimeout(() => controller.abort(), gatherTimeoutMs);
@@ -411,10 +414,13 @@ export async function gatherLaneCommand(key, flags, { request = devRequest, mark
411
414
  const pending = await timedRequest("GET", `/api/v1/lanes/${encode(key)}/amendments${query({ runId: lane.runId })}`);
412
415
  if (!pending.data.length) {
413
416
  const gathered = await withSignal(gather(key, flags, { request: timedRequest, signal }), signal);
414
- return { ...gathered, human: `${typeof gathered.human === "string" ? gathered.human : "Gather complete."}\nNo pending amendments.` };
417
+ if (gathered.payload?.data?.recorded === false) return gathered;
418
+ return { ...gathered, human: `${gatheredText(gathered)}\nNo pending amendments.` };
415
419
  }
416
420
  const version = Math.max(...pending.data.map((item) => item.version));
417
421
  const gathered = await withSignal(gather(key, flags, { request: timedRequest, signal }), signal);
422
+ // An agent picker's shortlist step records nothing, so it acknowledges nothing.
423
+ if (gathered.payload?.data?.recorded === false) return gathered;
418
424
  const acknowledgement = await withSignal(acknowledge(key, {
419
425
  ...flags,
420
426
  deltaContext: { runId: lane.runId, amendmentVersion: version },
@@ -424,7 +430,7 @@ export async function gatherLaneCommand(key, flags, { request = devRequest, mark
424
430
  throw new CliError("LANE_AMENDMENT_ACK_FAILED", `Delta gather did not acknowledge amendment version ${version}; the lane remains blocked.`, 1);
425
431
  return {
426
432
  payload: { ...gathered.payload, rules: acknowledgement.payload?.rules, amendments: pending.data, acknowledgedVersion },
427
- human: `${pending.data.map((item) => `v${item.version} ${item.reason}: ${JSON.stringify(item.change)}`).join("\n")}\n${typeof gathered.human === "string" ? gathered.human : "Gather complete."}\nAcknowledged amendment version ${version}.`,
433
+ human: `${pending.data.map((item) => `v${item.version} ${item.reason}: ${JSON.stringify(item.change)}`).join("\n")}\n${gatheredText(gathered)}\nAcknowledged amendment version ${version}.`,
428
434
  };
429
435
  } catch (error) {
430
436
  if (signal.aborted) throw new CliError("GATHER_TIMEOUT", `Gather exceeded its ${Math.round(gatherTimeoutMs / 1000)}-second deadline; nothing was retried.`, 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.32",
3
+ "version": "0.6.33",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {