@nodii/dev 0.6.28 → 0.6.29
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 +57 -0
- package/dist/api.js +4 -1
- package/dist/cli.js +35 -2
- package/dist/core.js +2 -2
- package/dist/doctor.js +1 -1
- package/dist/hooks.js +27 -1
- package/dist/lanes.js +58 -12
- package/dist/runtime-launcher.js +270 -0
- package/dist/safety.js +19 -0
- package/dist/setup.js +2 -2
- package/dist/starting.js +406 -0
- package/dist/watchdog.js +308 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -97,6 +97,8 @@ nodii-dev agent heartbeat
|
|
|
97
97
|
nodii-dev agents spawn SERVICE [--repo-root DIR]
|
|
98
98
|
nodii-dev agents cowork-tick [--json]
|
|
99
99
|
nodii-dev agents cowork-tick --plist [--interval SECONDS]
|
|
100
|
+
nodii-dev watchdog install [--interval SECONDS]
|
|
101
|
+
nodii-dev watchdog sweep
|
|
100
102
|
|
|
101
103
|
nodii-dev issues list [--service ID] [--project KEY] [--status STATUS]
|
|
102
104
|
[--kind KIND] [--priority PRIORITY] [--claimed true|false]
|
|
@@ -312,6 +314,7 @@ nodii-dev lane step KEY --side backend|frontend --start|--done|--block STEP [--r
|
|
|
312
314
|
nodii-dev lane evidence KEY --pr|--commit|--figma|--deploy|--dev-check REF
|
|
313
315
|
nodii-dev lane subagent-quota KEY N
|
|
314
316
|
nodii-dev lane heartbeat KEY [--note TEXT]
|
|
317
|
+
nodii-dev lane resume KEY
|
|
315
318
|
nodii-dev lane review KEY --file REVIEW.json
|
|
316
319
|
nodii-dev lane reviews KEY
|
|
317
320
|
nodii-dev lane land KEY
|
|
@@ -358,8 +361,22 @@ nodii-dev harness install [--include-drafts] [--adopt PATH,...] [--dry-run] [--s
|
|
|
358
361
|
nodii-dev harness update [--include-drafts] [--adopt PATH,...] [--dry-run] [--system]
|
|
359
362
|
nodii-dev harness check [--json] # 0 current, 1 differs from Dev, 2 no harness
|
|
360
363
|
nodii-dev harness update --check # the same check
|
|
364
|
+
nodii-dev watchdog install [--interval SECONDS] # install/update com.nodii.watchdog in ~/Library/LaunchAgents
|
|
365
|
+
nodii-dev watchdog sweep # launchd entrypoint; normally not run by hand
|
|
361
366
|
```
|
|
362
367
|
|
|
368
|
+
The per-Mac watchdog runs outside agent sessions. Run `watchdog install` once
|
|
369
|
+
with the machine coordinator's config; it refuses a lane or unassigned agent.
|
|
370
|
+
Every sweep reports three host facts to Dev: watched run processes that exited
|
|
371
|
+
without ending their run, `/tmp/cc-socks/<pid>.sock` sockets whose process is
|
|
372
|
+
gone, and watched worktrees whose HEAD has commits absent from its configured
|
|
373
|
+
remote branch. Each observation is posted to the watched run by the machine
|
|
374
|
+
coordinator; `process_dead` transitions it to explicit `orphan` immediately.
|
|
375
|
+
Run launchers and wrappers persist one owner-only record per process under
|
|
376
|
+
`~/.config/nodii-dev/watchdog/runs/`; the plist itself contains only absolute
|
|
377
|
+
command/config paths, its schedule, and log paths, while OAuth credentials
|
|
378
|
+
remain in the machine keychain.
|
|
379
|
+
|
|
363
380
|
`harness install` and `harness update` merge the safety hooks into both
|
|
364
381
|
`.claude/settings.json` and `.codex/hooks.json`; every installed hook invokes
|
|
365
382
|
only `nodii-dev`. The
|
|
@@ -718,6 +735,35 @@ from AGENT-HARNESS-14 is available.
|
|
|
718
735
|
|
|
719
736
|
## Build lanes and repository setup
|
|
720
737
|
|
|
738
|
+
`coordinator start --machine NAME` registers the machine's one watched
|
|
739
|
+
coordinator run and shows its running lanes, orphaned runs and unclaimed
|
|
740
|
+
results. A live second coordinator is refused; a coordinator whose heartbeat
|
|
741
|
+
watchdog has recorded it orphaned is recorded as the predecessor of the
|
|
742
|
+
takeover.
|
|
743
|
+
|
|
744
|
+
`lane launch KEY` replaces the manual worktree/start/process sequence. It
|
|
745
|
+
requires the issue (whose title supplies both lane title and objective), done
|
|
746
|
+
check, build kind and side, runtime profile and model, subagent quota, and
|
|
747
|
+
repeated `--repo NAME=/SOURCE` plus
|
|
748
|
+
`--branch NAME=BRANCH` pairs. The Hub preflight checks the coordinator waiter,
|
|
749
|
+
target agent and machine, fresh account-capacity telemetry, repository grants,
|
|
750
|
+
lane limit, open issues and (when `--figma` is present) the authenticated Figma
|
|
751
|
+
account. Only after that passes does the CLI create one worktree per repository,
|
|
752
|
+
atomically register the lane and watched run, and launch the runtime with the
|
|
753
|
+
single-line brief `You are lane KEY. Run nodii-dev lane gather.`. Ordinary
|
|
754
|
+
notes use `--notes`; a scope-narrowing note uses `--narrowing-note` and must
|
|
755
|
+
also name the remainder's destination with `--narrowing-remainder`.
|
|
756
|
+
The launched process uses the already-provisioned lowercase identity (for
|
|
757
|
+
example `~/.config/nodii-dev/agents/lane-ah-12.json`), never the coordinator's
|
|
758
|
+
Dev identity.
|
|
759
|
+
|
|
760
|
+
Objective, scope and rule changes are durable `lane amend` records, never chat
|
|
761
|
+
messages. Every CLI call from a launched worktree checks the current watched
|
|
762
|
+
run's acknowledgement version. A pending change is printed and blocks the
|
|
763
|
+
call until `lane gather --delta` serves the current rules and acknowledges the
|
|
764
|
+
newest displayed version. `NODII_DEV_RUN_ID` makes this check per subagent run,
|
|
765
|
+
so one child cannot acknowledge for its siblings.
|
|
766
|
+
|
|
721
767
|
`lane start` creates and starts a lane. It infers the repository (by short
|
|
722
768
|
name, as Dev compares it), branch and worktree from git unless overridden, and resolves `--issue` keys to the UUIDs
|
|
723
769
|
required by Dev. Lane keys are passed directly to every lane and Built
|
|
@@ -733,6 +779,17 @@ slot; Dev refuses another registration with `SUBAGENT_QUOTA_REACHED` when the
|
|
|
733
779
|
lane is full. A `done`, `handed_off`, or `failed` subagent run no longer counts,
|
|
734
780
|
so the watchdog marking a killed process failed frees the slot.
|
|
735
781
|
|
|
782
|
+
`lane resume` asks Dev to reserve a fresh run for an orphaned lane, then launches
|
|
783
|
+
the allowlisted Codex or Claude runtime as a detached headless process in the
|
|
784
|
+
same worktree. Dev keeps the lane's finished steps intact and returns the next
|
|
785
|
+
unfinished step together with its bounded recovery packet. The CLI sends that
|
|
786
|
+
packet over the new process's stdin (never its argv), saves its exact PID and
|
|
787
|
+
append-only log under `~/.config/nodii-dev/runs/<run-id>/`, and reports a local
|
|
788
|
+
spawn failure back to Dev so the reservation is not left running. Human output
|
|
789
|
+
identifies the replacement run, PID, step and last pushed commit without
|
|
790
|
+
echoing the dead run's log tail; `--json` retains the complete server response.
|
|
791
|
+
Dev refuses a third attempt after two identical deaths.
|
|
792
|
+
|
|
736
793
|
`setup` installs the Claude Code and Codex workflow harnesses in the current repository.
|
|
737
794
|
It writes `.claude/skills/nodii-build-workflow/SKILL.md`, merges SessionStart,
|
|
738
795
|
Stop and tool hooks into `.claude/settings.json` and `.codex/hooks.json`, and maintains one
|
package/dist/api.js
CHANGED
|
@@ -99,7 +99,8 @@ export const GROUPS = {
|
|
|
99
99
|
"approval-kinds": "ApprovalKinds", "notification-registry": "Notification registry", skills: "Skills", "open-questions": "OpenQuestions",
|
|
100
100
|
decisions: "Decisions", drift: "Drift", requests: "Requests", pipelines: "Pipelines", slices: "Slices", "sql-lookups": "SqlLookups",
|
|
101
101
|
runners: "Runners", routing: "Routing", usage: "Usage", audit: "AuditLog", reconciliation: "Reconciliation", cron: "Cron", health: "Health",
|
|
102
|
-
composite: "Composite", "service-edges": "ServiceEdges", lanes: "Lanes",
|
|
102
|
+
composite: "Composite", "service-edges": "ServiceEdges", lanes: "Lanes", runs: "Runs",
|
|
103
|
+
releases: "Releases", operate: "Operate",
|
|
103
104
|
swaps: "Swaps", ideas: "Ideas", terminology: "Terminology", credentials: "Credentials",
|
|
104
105
|
rulebook: "ad-documents", "ad-decisions": "ad-decisions", domains: "ad-domains", standardizations: "standardizations",
|
|
105
106
|
archive: "archive", harness: "harness", utils: "utils",
|
|
@@ -154,6 +155,8 @@ export const ALIASES = {
|
|
|
154
155
|
"standardizations:list": "listStandardizations", "standardizations:show": "getStandardization", "standardizations:update": "updateStandardization",
|
|
155
156
|
"archive:search": "searchPreDevArchive", "archive:resolve": "resolvePreCitation",
|
|
156
157
|
"harness:show": "generateHarness", "harness:methodology": "getHarnessMethodologyVersion",
|
|
158
|
+
"runs:register": "registerRun", "runs:show": "getRun", "runs:heartbeat": "heartbeatRun", "runs:end": "endRun",
|
|
159
|
+
"runs:results": "listRunResults", "runs:acknowledge": "acknowledgeResult",
|
|
157
160
|
"terminology:list": "listTerminology", "terminology:show": "getTerminologyTerm", "terminology:create": "createTerminologyTerm", "terminology:update": "updateTerminologyTerm",
|
|
158
161
|
};
|
|
159
162
|
|
package/dist/cli.js
CHANGED
|
@@ -53,6 +53,7 @@ export { figmaLock, submitMode } from "./figma.js";
|
|
|
53
53
|
import { docsCommand, rulebookCommand, rulesCommand } from "./upload.js";
|
|
54
54
|
import { changeSetCommand, guidelineCommand } from "./guidelines.js";
|
|
55
55
|
import { designCheck, designCheckText, designRoles, designRolesText } from "./design.js";
|
|
56
|
+
import { watchdogCommand } from "./watchdog.js";
|
|
56
57
|
import {
|
|
57
58
|
ALIASES,
|
|
58
59
|
GROUPS,
|
|
@@ -74,6 +75,13 @@ import {
|
|
|
74
75
|
nextCommand,
|
|
75
76
|
peopleCommand,
|
|
76
77
|
} from "./lanes.js";
|
|
78
|
+
import {
|
|
79
|
+
amendmentGuard,
|
|
80
|
+
amendLaneCommand,
|
|
81
|
+
coordinatorCommand,
|
|
82
|
+
gatherLaneCommand,
|
|
83
|
+
launchLaneCommand,
|
|
84
|
+
} from "./starting.js";
|
|
77
85
|
import {
|
|
78
86
|
launchRunSupervisor,
|
|
79
87
|
showWaitingResults,
|
|
@@ -132,6 +140,8 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
132
140
|
# --system stages managed hooks and only prints operator-reviewed sudo commands
|
|
133
141
|
harness check # 0 current, 1 differs from Dev, 2 not installed
|
|
134
142
|
harness update --check # the same check
|
|
143
|
+
watchdog install [--interval SECONDS] # install/update the per-Mac launchd liveness sweep (default 60s)
|
|
144
|
+
watchdog sweep # launchd entrypoint: report dead runs, stale sockets, unpushed worktrees
|
|
135
145
|
docs check|validate|upload <path...> --class ruling|conformance|editorial
|
|
136
146
|
[--base CODE=VERSION] [--decision FILE] [--pending] [--conformance-ref REF]
|
|
137
147
|
[--meaning-unchanged REASON] [--wait]
|
|
@@ -142,6 +152,17 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
142
152
|
rules report [--section S] [--lane KEY] # delivery totals per document; rules served per lane
|
|
143
153
|
rules cap TOKENS # operator: set the core token cap (default 3000)
|
|
144
154
|
|
|
155
|
+
coordinator start --machine NAME # register this machine's one watched coordinator; refuses a live second
|
|
156
|
+
lane launch <key> --done-check TEXT --kind KIND --side SIDE
|
|
157
|
+
--issue KEY --repo NAME=/SOURCE --branch NAME=BRANCH [--repo ... --branch ...]
|
|
158
|
+
--machine NAME --runtime claude|codex --profile PATH --model MODEL --subagents N
|
|
159
|
+
[--base NAME=REF] [--worktree NAME=PATH] [--owned-path NAME=PATH]
|
|
160
|
+
[--figma FILE[=COMPONENT_SET[,SET]]] [--notes TEXT]
|
|
161
|
+
[--narrowing-note TEXT --narrowing-remainder DESTINATION]
|
|
162
|
+
# authoritative preflight, one worktree per repo, watched launch
|
|
163
|
+
# uses the provisioned agents/lane-<key>.json Dev identity
|
|
164
|
+
lane amend <key> --objective|--scope|--rules TEXT --reason TEXT
|
|
165
|
+
lane gather [<key>] [--delta] # infers a launched worktree; Delta required after an amendment
|
|
145
166
|
run [--kind build|test_run|subagent|deploy] [--name TEXT] -- <command> [args...]
|
|
146
167
|
# watched command: registers with NODII_DEV_RUN_ID,
|
|
147
168
|
# heartbeats, and records exit code + redacted log tail
|
|
@@ -151,7 +172,7 @@ Usage: nodii-dev <command> [options] [--json]
|
|
|
151
172
|
# --block --reason on a blocked step records its new reason
|
|
152
173
|
lane evidence <key> --pr|--commit|--figma|--deploy|--dev-check REF
|
|
153
174
|
lane subagent-quota <key> <n> # coordinator assigns 0-100 subagent slots
|
|
154
|
-
lane heartbeat|land|verify|prioritize|deprioritize|stop <key>
|
|
175
|
+
lane heartbeat|resume|land|verify|prioritize|deprioritize|stop <key>
|
|
155
176
|
lane review <key> --file REVIEW.json # base/head, outcome and clause-cited findings
|
|
156
177
|
lane reviews <key> # stored review history for the lane
|
|
157
178
|
lane non-issue <key> --reason TEXT --evidence TEXT --reroute KEY[,KEY]|none
|
|
@@ -412,8 +433,12 @@ export async function figmaFileAdd(value, flags, { request = devRequest } = {})
|
|
|
412
433
|
}
|
|
413
434
|
|
|
414
435
|
export async function main(argv = process.argv.slice(2)) {
|
|
415
|
-
|
|
436
|
+
// Amendment acknowledgement is a lane-wide backstop, including otherwise
|
|
437
|
+
// local commands such as help and version.
|
|
438
|
+
if (argv[0] !== "__run-supervisor") {
|
|
439
|
+
await amendmentGuard(argv);
|
|
416
440
|
await showWaitingResults({ acknowledge: argv[0] !== "hook" });
|
|
441
|
+
}
|
|
417
442
|
if (argv.length === 1 && ["--version", "-v"].includes(argv[0]))
|
|
418
443
|
return console.log(VERSION);
|
|
419
444
|
if (argv.length === 1 && ["--help", "-h"].includes(argv[0]))
|
|
@@ -523,6 +548,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
523
548
|
if (rest.length) throw new CliError("USAGE", `harness ${action} takes no arguments, got: ${rest.join(" ")}. Write a flag value as --include-drafts=false.`, 2);
|
|
524
549
|
return emitCommand(await harnessCommand(action, f));
|
|
525
550
|
}
|
|
551
|
+
if (command === "watchdog") {
|
|
552
|
+
if (rest.length) throw new CliError("USAGE", `watchdog ${action ?? ""} takes no arguments, got: ${rest.join(" ")}.`, 2);
|
|
553
|
+
return emitCommand(await watchdogCommand(action, f));
|
|
554
|
+
}
|
|
526
555
|
if (command === "docs") return emitCommand(await docsCommand(action, rest, f));
|
|
527
556
|
if (command === "rulebook" && ["list", "show", "diff"].includes(action)) return emitCommand(await rulebookCommand(action, rest, f));
|
|
528
557
|
if (command === "rules") return emitCommand(await rulesCommand(action, rest, f));
|
|
@@ -541,6 +570,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
541
570
|
}
|
|
542
571
|
return;
|
|
543
572
|
}
|
|
573
|
+
if (command === "coordinator") return emitCommand(await coordinatorCommand(action, f));
|
|
574
|
+
if (command === "lane" && action === "launch") return emitCommand(await launchLaneCommand(rest[0], f));
|
|
575
|
+
if (command === "lane" && action === "amend") return emitCommand(await amendLaneCommand(rest[0], f));
|
|
576
|
+
if (command === "lane" && action === "gather") return emitCommand(await gatherLaneCommand(rest[0], f));
|
|
544
577
|
if (command === "lane") return emitCommand(await laneCommand(action, rest, f));
|
|
545
578
|
if (command === "next") return emitCommand(await nextCommand(action, f));
|
|
546
579
|
if (command === "built") return emitCommand(await builtCommand(action, rest, f));
|
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.29";
|
|
8
8
|
export const configPath = () =>
|
|
9
9
|
process.env.NODII_DEV_CONFIG_PATH ??
|
|
10
10
|
join(homedir(), ".config/nodii-dev/config.json");
|
|
@@ -199,7 +199,7 @@ export async function checkLogin() {
|
|
|
199
199
|
export const encode = (value) => encodeURIComponent(value);
|
|
200
200
|
// Switches never take the next token as their value, so
|
|
201
201
|
// `docs upload --wait rulebook/` keeps rulebook/ as a path.
|
|
202
|
-
const SWITCHES = new Set(["json", "wait", "pending", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted", "library", "async", "sync", "force", "system"]);
|
|
202
|
+
const SWITCHES = new Set(["json", "wait", "pending", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted", "library", "async", "sync", "force", "system", "delta"]);
|
|
203
203
|
export function parseArgs(argv) {
|
|
204
204
|
const flags = {};
|
|
205
205
|
const args = [];
|
package/dist/doctor.js
CHANGED
|
@@ -19,7 +19,7 @@ export const LANE_PERMISSIONS = [
|
|
|
19
19
|
{ key: "devops.issues.read", for: "issues list|show" },
|
|
20
20
|
{ key: "devops.issues.write", for: "issues create|update|claim|release|transition" },
|
|
21
21
|
{ key: "devops.lanes.read", for: "lane show|list, next" },
|
|
22
|
-
{ key: "devops.lanes.write", for: "lane start|step|evidence|heartbeat|review|land|verify" },
|
|
22
|
+
{ key: "devops.lanes.write", for: "lane launch|amend|start|step|evidence|heartbeat|review|land|verify" },
|
|
23
23
|
{ key: "devops.agents.read", for: "whoami, reading this agent" },
|
|
24
24
|
{ key: "devops.agents.write", for: "agent register|heartbeat" },
|
|
25
25
|
{ key: "devops.machines.manage", for: "machine show (this agent's machine and coordinator)" },
|
package/dist/hooks.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError, devRequest, readConfig } from "./core.js";
|
|
2
2
|
import { FOUNDATIONS_ID, figmaLockCheck, outOfScopeText } from "./figma.js";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
-
import { realpath } from "node:fs/promises";
|
|
4
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { safetySettingsSnippet } from "./safety.js";
|
|
@@ -209,6 +209,15 @@ export async function heartbeatCurrentLane(request = devRequest) {
|
|
|
209
209
|
try { branch = (await execFileAsync("git", ["branch", "--show-current"], { encoding: "utf8" })).stdout.trim(); }
|
|
210
210
|
catch { return { heartbeated: false, reason: "not a git worktree" }; }
|
|
211
211
|
if (!branch) return { heartbeated: false, reason: "detached HEAD" };
|
|
212
|
+
try {
|
|
213
|
+
const markerPath = (await execFileAsync("git", ["rev-parse", "--path-format=absolute", "--git-path", "nodii-lane.json"], { encoding: "utf8" })).stdout.trim();
|
|
214
|
+
const marker = JSON.parse(await readFile(markerPath, "utf8"));
|
|
215
|
+
const lane = (await request("GET", `/api/v1/lanes/${encodeURIComponent(marker.laneKey)}`)).data;
|
|
216
|
+
if (lane.repositories?.some((repository) => repository.branch === branch)) {
|
|
217
|
+
await request("POST", `/api/v1/lanes/${encodeURIComponent(lane.id)}/heartbeat`, { via: "nodii-dev" });
|
|
218
|
+
return { heartbeated: true, key: lane.key };
|
|
219
|
+
}
|
|
220
|
+
} catch {}
|
|
212
221
|
const lanes = (await request("GET", "/api/v1/lanes?limit=200")).data ?? [];
|
|
213
222
|
const lane = lanes.find((x) => x.branch === branch && !["on_dev", "stopped"].includes(x.status));
|
|
214
223
|
if (!lane) return { heartbeated: false, reason: "no lane for current branch" };
|
|
@@ -226,6 +235,23 @@ export async function sessionStartContext(payload, { request = devRequest, confi
|
|
|
226
235
|
const rootText = (await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" })).stdout.trim();
|
|
227
236
|
const root = await realpath(rootText).catch(() => resolve(rootText));
|
|
228
237
|
const current = await config(false);
|
|
238
|
+
try {
|
|
239
|
+
const markerPath = (await execFileAsync("git", ["rev-parse", "--path-format=absolute", "--git-path", "nodii-lane.json"], { cwd, encoding: "utf8" })).stdout.trim();
|
|
240
|
+
const marker = JSON.parse(await readFile(markerPath, "utf8"));
|
|
241
|
+
const marked = (await request("GET", `/api/v1/lanes/${encodeURIComponent(marker.laneKey)}`)).data;
|
|
242
|
+
const repository = marked.repositories?.find((item) => item.branch === branch && resolve(item.worktree) === root);
|
|
243
|
+
if (marked.agentId === current.agentId && repository && !["on_dev", "stopped"].includes(marked.status)) lane = marked;
|
|
244
|
+
} catch {}
|
|
245
|
+
if (lane) {
|
|
246
|
+
const freeze = lane.freeze ?? {};
|
|
247
|
+
return [
|
|
248
|
+
`Freeze re-grounding for ${lane.key}:`,
|
|
249
|
+
`title: ${lane.title ?? "(untitled)"}`,
|
|
250
|
+
`status: ${lane.status}`,
|
|
251
|
+
`boundaries: ${JSON.stringify(freeze.boundaries ?? {})}`,
|
|
252
|
+
`rules: ${JSON.stringify(freeze.rules ?? freeze.ruleRefs ?? [])}`,
|
|
253
|
+
].join("\n");
|
|
254
|
+
}
|
|
229
255
|
const lanes = (await request("GET", "/api/v1/lanes?limit=200")).data ?? [];
|
|
230
256
|
for (const item of lanes) {
|
|
231
257
|
if (item.agentId !== current.agentId || item.branch !== branch || ["on_dev", "stopped"].includes(item.status)) continue;
|
package/dist/lanes.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { CliError, encode, devRequest, query, readConfig, requireFlag } from "./core.js";
|
|
5
|
+
import { launchRecovery } from "./runtime-launcher.js";
|
|
5
6
|
import { recordReviewReceipt, reviewCommitContext } from "./safety.js";
|
|
6
7
|
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
@@ -106,14 +107,18 @@ const MAX_COMMITS = 200;
|
|
|
106
107
|
/** The lane's branch commits not on origin/main (as last fetched), from its recorded worktree. */
|
|
107
108
|
export async function branchCommits(lane) {
|
|
108
109
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
110
|
+
const repositories = lane.repositories?.length ? lane.repositories : [{ worktree: lane.worktree, branch: lane.branch, baseRef: "origin/main" }];
|
|
111
|
+
const commits = await Promise.all(repositories.map(async (repository) => {
|
|
112
|
+
const out = (await execFileAsync("git", ["-C", repository.worktree, "rev-list", `${repository.baseRef ?? "origin/main"}..${repository.branch}`], { encoding: "utf8" })).stdout.trim();
|
|
113
|
+
return out ? out.split("\n") : [];
|
|
114
|
+
}));
|
|
115
|
+
return [...new Set(commits.flat())];
|
|
111
116
|
} catch {
|
|
112
|
-
throw new CliError("GIT_REQUIRED", `Cannot read
|
|
117
|
+
throw new CliError("GIT_REQUIRED", `Cannot read every repository branch for this lane. Pass --commits SHA[,SHA], or --branch dropped:REASON.`, 2);
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
export async function laneCommand(action, args, flags, { request = devRequest, config = readConfig, gitContext = inferGitContext, commitsOf = branchCommits, read = (path) => readFile(path, "utf8"), reviewContext = reviewCommitContext, reviewReceipt = recordReviewReceipt } = {}) {
|
|
121
|
+
export async function laneCommand(action, args, flags, { request = devRequest, config = readConfig, gitContext = inferGitContext, commitsOf = branchCommits, read = (path) => readFile(path, "utf8"), reviewContext = reviewCommitContext, reviewReceipt = recordReviewReceipt, launch = launchRecovery } = {}) {
|
|
117
122
|
if (action === "list") {
|
|
118
123
|
const c = flags.mine ? await config() : null;
|
|
119
124
|
const payload = await request("GET", `/api/v1/lanes${query({ kind: flags.kind, status: flags.status, machine: flags.machine, agent: flags.mine ? c.agentId : flags.agent, "waiting-on": flags["waiting-on"], stuck: flags.stuck, cursor: flags.cursor, limit: flags.limit })}`);
|
|
@@ -194,6 +199,40 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
|
|
|
194
199
|
);
|
|
195
200
|
return { payload, human: `${ref}: subagent quota set to ${quota}.` };
|
|
196
201
|
}
|
|
202
|
+
if (action === "resume") {
|
|
203
|
+
const payload = await request(
|
|
204
|
+
"POST",
|
|
205
|
+
`/api/v1/lanes/${encode(id)}/resume`,
|
|
206
|
+
via,
|
|
207
|
+
);
|
|
208
|
+
let launched;
|
|
209
|
+
try {
|
|
210
|
+
launched = await launch(payload);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const reason = `local recovery launch failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2_000);
|
|
213
|
+
const runId = payload?.data?.run?.id;
|
|
214
|
+
if (runId) {
|
|
215
|
+
await request(
|
|
216
|
+
"POST",
|
|
217
|
+
`/api/v1/lanes/${encode(id)}/resume/${encode(runId)}/launch-failed`,
|
|
218
|
+
{ reason, ...via },
|
|
219
|
+
).catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
throw new CliError("RECOVERY_LAUNCH_FAILED", reason, 1);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
payload,
|
|
225
|
+
human: (response) => {
|
|
226
|
+
const runId = response.data.run.id;
|
|
227
|
+
const next = response.data.lane.next?.[0];
|
|
228
|
+
const checkpoint = response.data.recovery.lastPushedCommit;
|
|
229
|
+
const continuation = next
|
|
230
|
+
? `${next.side}/${next.step}`
|
|
231
|
+
: "the lane's next unfinished step";
|
|
232
|
+
return `${ref}: resumed fresh run ${runId} as PID ${launched.pid}; continuing ${continuation} from ${checkpoint}.`;
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
197
236
|
if (action === "land") return { payload: await request("POST", `/api/v1/lanes/${encode(id)}/land`, via), human: `${ref}: recorded on main.` };
|
|
198
237
|
if (action === "review") {
|
|
199
238
|
const body = await reviewBody(flags, read);
|
|
@@ -269,11 +308,15 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
|
|
|
269
308
|
*/
|
|
270
309
|
export async function laneChangedPaths(lane) {
|
|
271
310
|
try {
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
311
|
+
const repositories = lane.repositories?.length ? lane.repositories : [{ repo: lane.repos?.[0], worktree: lane.worktree, baseRef: "origin/main" }];
|
|
312
|
+
const changed = await Promise.all(repositories.map(async (repository) => {
|
|
313
|
+
const run = async (...args) => (await execFileAsync("git", ["-C", repository.worktree, ...args], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 })).stdout;
|
|
314
|
+
const base = (await run("merge-base", repository.baseRef ?? "origin/main", "HEAD")).trim();
|
|
315
|
+
const tracked = (await run("diff", "--name-only", base)).split("\n");
|
|
316
|
+
const untracked = (await run("ls-files", "--others", "--exclude-standard")).split("\n");
|
|
317
|
+
return [...tracked, ...untracked].map((path) => path.trim()).filter(Boolean);
|
|
318
|
+
}));
|
|
319
|
+
return [...new Set(changed.flat())].slice(0, 5000);
|
|
277
320
|
} catch {
|
|
278
321
|
return null;
|
|
279
322
|
}
|
|
@@ -295,11 +338,14 @@ export async function nextCommand(ref, flags, { request = devRequest, changedPat
|
|
|
295
338
|
const data = steps.map((step) => ({ side: step.side, step: step.stepKey, label: step.label, state: step.state, command: `nodii-dev lane step ${lane.key} --side ${step.side} --${step.state === "current" ? "done" : "start"} ${step.stepKey}` }));
|
|
296
339
|
// M05-32: the clauses Dev serves for this step. A failure here never hides the next step.
|
|
297
340
|
let rules = null;
|
|
298
|
-
if (data.length) {
|
|
341
|
+
if (data.length || flags.deltaContext) {
|
|
299
342
|
const paths = flags.paths ? list(flags.paths) : await changedPaths(lane);
|
|
300
|
-
const body = { ...(paths ? { paths } : {}), ...(flags.components ? { components: list(flags.components) } : {}), ...(flags.role ? { role: String(flags.role) } : {}) };
|
|
343
|
+
const body = { ...(paths ? { paths } : {}), ...(flags.components ? { components: list(flags.components) } : {}), ...(flags.role ? { role: String(flags.role) } : {}), ...(flags.deltaContext && typeof flags.deltaContext === "object" ? { delta: flags.deltaContext } : {}) };
|
|
301
344
|
try { rules = (await request("POST", `/api/v1/lanes/${encode(lane.key)}/rules`, body)).data; }
|
|
302
|
-
catch (error) {
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (flags.deltaContext) throw error;
|
|
347
|
+
rules = { error: error.code ?? error.message };
|
|
348
|
+
}
|
|
303
349
|
}
|
|
304
350
|
const human = data.length
|
|
305
351
|
? [...data.map((x) => `${x.side}: ${x.label}\n ${x.command}`), ...rulesLines(rules, lane.key)].join("\n")
|