@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 +8 -0
- package/dist/core.js +1 -1
- package/dist/lanes.js +85 -9
- package/dist/starting.js +8 -2
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 --
|
|
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
|
-
|
|
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${
|
|
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);
|