@nodii/dev 0.6.26 → 0.6.28

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
@@ -135,6 +135,8 @@ nodii-dev libraries register-version LIBRARY_ID --version VERSION
135
135
  nodii-dev state diff
136
136
  nodii-dev heartbeat audit [SERVICE]
137
137
 
138
+ nodii-dev run [--kind build|test_run|subagent|deploy] [--name TEXT] -- COMMAND [ARGS...]
139
+
138
140
  nodii-dev design entries [--kind KIND] [--group GROUP] [--q TEXT]
139
141
  nodii-dev design entry KEY
140
142
  nodii-dev design guide PAGE_TYPE
@@ -308,6 +310,7 @@ nodii-dev lane step KEY --side backend|frontend --start|--done|--block STEP [--r
308
310
  # --reason is kept as a lane note; on a step already blocked,
309
311
  # --block --reason records the new reason (Dev issue 125 moves it onto the step)
310
312
  nodii-dev lane evidence KEY --pr|--commit|--figma|--deploy|--dev-check REF
313
+ nodii-dev lane subagent-quota KEY N
311
314
  nodii-dev lane heartbeat KEY [--note TEXT]
312
315
  nodii-dev lane review KEY --file REVIEW.json
313
316
  nodii-dev lane reviews KEY
@@ -345,7 +348,7 @@ nodii-dev rulebook diff CODE [--from V] [--to V]
345
348
  nodii-dev docs check PATH... # local M-05 checks only
346
349
  nodii-dev docs validate PATH... --class CLASS # Dev's dry run of the upload
347
350
  nodii-dev docs upload PATH... --class ruling|conformance|editorial
348
- [--base CODE=VERSION ...] [--decision DECISION.json]
351
+ [--base CODE=VERSION ...] [--decision DECISION.json] [--pending]
349
352
  [--conformance-ref PR|REPO@SHA] [--meaning-unchanged REASON]
350
353
  [--wait [--wait-minutes N]]
351
354
  nodii-dev docs ratify --domain KEY [--domain KEY ...] --decision DECISION.json [--wait]
@@ -542,9 +545,12 @@ Dev's error codes, and nothing is sent until there are none. Dev then runs the
542
545
  same checks, plus the ones that need its data, in one transaction.
543
546
 
544
547
  - `--class` is required on every write (M05-8). A `ruling` carries its
545
- decision (`--decision`, with `amends`), and the decision locks with the new
546
- versions or nothing is written. An `editorial` write that changes a Rules
547
- clause is refused unless `--meaning-unchanged REASON` is given (M05-10).
548
+ decision (`--decision`, with `amends`). Agents must add `--pending`: Dev
549
+ stores the ruling versions under a pending decision without activating
550
+ them, and a signed-in staff person locks it later. Omitting `--pending`
551
+ requests the staff-only immediate lock-and-amend action. An `editorial`
552
+ write that changes a Rules clause is refused unless
553
+ `--meaning-unchanged REASON` is given (M05-10).
548
554
  - A new document is based on nothing. For a document Dev already has, pass
549
555
  the version you started from with `--base CODE=VERSION` (M05-11). The CLI
550
556
  never fills it in from Dev, so a stale edit is refused instead of
@@ -686,6 +692,30 @@ Doctor may refresh the access token, as any command does. The permission table
686
692
  is kept by hand from Dev's route policy until Dev's OpenAPI names each
687
693
  operation's permission (requested in Dev).
688
694
 
695
+ ## Watched commands and Results
696
+
697
+ The harness launcher supplies `NODII_DEV_RUN_ID`, the UUID of the current lane,
698
+ coordinator, or subagent run. Use `nodii-dev run -- COMMAND ARGS...` for work
699
+ whose completion must survive the calling agent. The command registers a child
700
+ run with that UUID as its waiter, starts an out-of-process supervisor, and
701
+ preserves the command's exit code. `--kind` defaults to `build`; `--name`
702
+ overrides the executable name shown in Dev without storing command arguments.
703
+
704
+ The supervisor heartbeats once a minute while the command runs. At completion
705
+ it records the executable, exit code, signal when applicable, and the final 8
706
+ KiB of combined stdout/stderr. Credential-shaped values, bearer tokens, private
707
+ keys, and credentials embedded in URLs are redacted before the tail leaves the
708
+ machine. The command is executed directly, never through a shell. Because the
709
+ supervisor is detached from the launching process, the terminal record still
710
+ lands when that process is killed.
711
+
712
+ Every subsequent `nodii-dev` call in the waiter context prints unacknowledged
713
+ Results before doing its requested work. A Result is acknowledged only after
714
+ it renders successfully. Hook calls deliberately leave Results pending because
715
+ their output may be swallowed; the next visible call prints and acknowledges
716
+ them. Unacknowledged amendments will join this preflight when the amendment API
717
+ from AGENT-HARNESS-14 is available.
718
+
689
719
  ## Build lanes and repository setup
690
720
 
691
721
  `lane start` creates and starts a lane. It infers the repository (by short
@@ -697,6 +727,12 @@ serves for that step (see Rule delivery). Server refusals,
697
727
  including operator-only actions, workflow blocks, lane limits and Built items
698
728
  that are not on dev, retain Dev's error code/message and exit non-zero.
699
729
 
730
+ The machine coordinator sets a lane's subagent capacity with `lane
731
+ subagent-quota KEY N` (`0` to `100`). Registering a subagent run consumes one
732
+ slot; Dev refuses another registration with `SUBAGENT_QUOTA_REACHED` when the
733
+ lane is full. A `done`, `handed_off`, or `failed` subagent run no longer counts,
734
+ so the watchdog marking a killed process failed frees the slot.
735
+
700
736
  `setup` installs the Claude Code and Codex workflow harnesses in the current repository.
701
737
  It writes `.claude/skills/nodii-build-workflow/SKILL.md`, merges SessionStart,
702
738
  Stop and tool hooks into `.claude/settings.json` and `.codex/hooks.json`, and maintains one
@@ -778,7 +814,8 @@ runtime, people, agents, inbox/gates, design, runbooks,
778
814
  doctrine and exceptions, changelog, health verdicts, approval kinds,
779
815
  notification registry, skills, questions, decisions, drift, requests,
780
816
  pipelines, runners, routing, usage, audit, reconciliation, cron, composite
781
- surfaces, service edges, lanes, swaps, ideas, terminology, and the AD era: `rulebook`
817
+ surfaces, service edges, lanes, swaps, ideas, terminology, watched runs,
818
+ releases, operate surfaces, and the AD era: `rulebook`
782
819
  (documents, plus `rulebook versions CODE`), `ad-decisions`, `domains`, `standardizations`,
783
820
  `archive` (`archive resolve --ref pre:D1366`), and `harness methodology`. `api list` is the authoritative discovery view;
784
821
  `api OPERATION_ID` makes every operation callable even without an alias.
package/dist/api.js CHANGED
@@ -103,6 +103,7 @@ export const GROUPS = {
103
103
  swaps: "Swaps", ideas: "Ideas", terminology: "Terminology", credentials: "Credentials",
104
104
  rulebook: "ad-documents", "ad-decisions": "ad-decisions", domains: "ad-domains", standardizations: "standardizations",
105
105
  archive: "archive", harness: "harness", utils: "utils",
106
+ runs: "Runs", releases: "Releases", operate: "Operate",
106
107
  // DEC-0020 rule delivery; `rules core|report|cap` are first-class (upload.js).
107
108
  rules: "rulebook",
108
109
  };
package/dist/cli.js CHANGED
@@ -74,6 +74,11 @@ import {
74
74
  nextCommand,
75
75
  peopleCommand,
76
76
  } from "./lanes.js";
77
+ import {
78
+ launchRunSupervisor,
79
+ showWaitingResults,
80
+ superviseRun,
81
+ } from "./runs.js";
77
82
 
78
83
  const HELP = `nodii-dev ${VERSION}
79
84
 
@@ -128,7 +133,7 @@ Usage: nodii-dev <command> [options] [--json]
128
133
  harness check # 0 current, 1 differs from Dev, 2 not installed
129
134
  harness update --check # the same check
130
135
  docs check|validate|upload <path...> --class ruling|conformance|editorial
131
- [--base CODE=VERSION] [--decision FILE] [--conformance-ref REF]
136
+ [--base CODE=VERSION] [--decision FILE] [--pending] [--conformance-ref REF]
132
137
  [--meaning-unchanged REASON] [--wait]
133
138
  docs ratify --domain KEY [--domain KEY ...] --decision FILE [--wait]
134
139
  docs parked [CODE] [--status pending|acknowledged]
@@ -137,10 +142,15 @@ Usage: nodii-dev <command> [options] [--json]
137
142
  rules report [--section S] [--lane KEY] # delivery totals per document; rules served per lane
138
143
  rules cap TOKENS # operator: set the core token cap (default 3000)
139
144
 
145
+ run [--kind build|test_run|subagent|deploy] [--name TEXT] -- <command> [args...]
146
+ # watched command: registers with NODII_DEV_RUN_ID,
147
+ # heartbeats, and records exit code + redacted log tail
148
+
140
149
  lane start <key> --title TEXT --kind KIND --side SIDE [--repo REPO] [--issue ISSUE]
141
150
  lane step <key> --side SIDE --start|--done|--block <step> [--reason TEXT]
142
151
  # --block --reason on a blocked step records its new reason
143
152
  lane evidence <key> --pr|--commit|--figma|--deploy|--dev-check REF
153
+ lane subagent-quota <key> <n> # coordinator assigns 0-100 subagent slots
144
154
  lane heartbeat|land|verify|prioritize|deprioritize|stop <key>
145
155
  lane review <key> --file REVIEW.json # base/head, outcome and clause-cited findings
146
156
  lane reviews <key> # stored review history for the lane
@@ -164,7 +174,8 @@ contracts, sagas, outbox-topics, tables, libraries, workers, projects, issues,
164
174
  backlogs, builds, changes, deploy-reports, runtime, people, agents, inbox,
165
175
  gate, design, runbooks, doctrine, changelog, health-verdicts, approval-kinds,
166
176
  notification-registry, skills, open-questions, decisions, drift, requests,
167
- rulebook, ad-decisions, domains, standardizations, archive, harness, and utils.
177
+ rulebook, ad-decisions, domains, standardizations, archive, harness, utils,
178
+ runs, releases, and operate.
168
179
 
169
180
  Run a command with --help or see the package README for all flags.`;
170
181
 
@@ -401,10 +412,51 @@ export async function figmaFileAdd(value, flags, { request = devRequest } = {})
401
412
  }
402
413
 
403
414
  export async function main(argv = process.argv.slice(2)) {
415
+ if (argv[0] !== "__run-supervisor")
416
+ await showWaitingResults({ acknowledge: argv[0] !== "hook" });
404
417
  if (argv.length === 1 && ["--version", "-v"].includes(argv[0]))
405
418
  return console.log(VERSION);
406
419
  if (argv.length === 1 && ["--help", "-h"].includes(argv[0]))
407
420
  return console.log(HELP);
421
+ if (["run", "__run-supervisor"].includes(argv[0])) {
422
+ const boundary = argv.indexOf("--");
423
+ if (boundary === -1 || boundary === argv.length - 1)
424
+ throw new CliError("USAGE", "run needs a command after `--`.", 2);
425
+ const { args: optionArgs, flags: runFlags } = parseArgs(
426
+ argv.slice(1, boundary),
427
+ );
428
+ if (optionArgs.length)
429
+ throw new CliError(
430
+ "USAGE",
431
+ `Unexpected run argument before \`--\`: ${optionArgs.join(" ")}`,
432
+ 2,
433
+ );
434
+ const kind = runFlags.kind;
435
+ const name = runFlags.name;
436
+ if (Array.isArray(kind) || kind === true)
437
+ throw new CliError("USAGE", "run --kind needs one value.", 2);
438
+ if (Array.isArray(name) || name === true)
439
+ throw new CliError("USAGE", "run --name needs one value.", 2);
440
+ const command = argv.slice(boundary + 1);
441
+ if (argv[0] === "__run-supervisor") {
442
+ const ignoreBrokenPipe = (error) => {
443
+ if (error?.code !== "EPIPE") throw error;
444
+ };
445
+ stdout.on("error", ignoreBrokenPipe);
446
+ process.stderr.on("error", ignoreBrokenPipe);
447
+ try {
448
+ const result = await superviseRun({ command, kind, name });
449
+ process.exitCode = result.exitCode;
450
+ return;
451
+ } finally {
452
+ stdout.off("error", ignoreBrokenPipe);
453
+ process.stderr.off("error", ignoreBrokenPipe);
454
+ }
455
+ }
456
+ const exitCode = await launchRunSupervisor(command, { kind, name });
457
+ process.exitCode = exitCode;
458
+ return;
459
+ }
408
460
  const { args, flags: f } = parseArgs(argv);
409
461
  const json = Boolean(f.json);
410
462
  const [command, action, ...rest] = args;
@@ -766,10 +818,6 @@ export async function main(argv = process.argv.slice(2)) {
766
818
  return;
767
819
  }
768
820
  if (["claude-figma-pre", "figma-pre"].includes(action)) {
769
- // Fail closed: Claude Code only blocks a tool call on exit code 2, and
770
- // treats any other non-zero exit as a non-blocking error that lets the
771
- // write through. Every failure here (bad stdin, missing config, Dev
772
- // error, lock denied) must therefore exit 2.
773
821
  let payload;
774
822
  try {
775
823
  payload = JSON.parse(await stdinText());
@@ -778,6 +826,9 @@ export async function main(argv = process.argv.slice(2)) {
778
826
  if (hookRuntime(payload) !== "codex") return emit(result, json, "allowed");
779
827
  return;
780
828
  } catch (error) {
829
+ // A known policy denial still blocks. Hook/runtime failures fail open
830
+ // and stay silent so a broken environment cannot stop the session.
831
+ if (!["FIGMA_FILE_KEY_MISSING", "FIGMA_LOCK_DENIED"].includes(error?.code)) return;
781
832
  if (payload && hookRuntime(payload) === "codex") {
782
833
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: `Blocked Figma write: ${error?.message ?? String(error)}` } }));
783
834
  return;
@@ -791,7 +842,6 @@ export async function main(argv = process.argv.slice(2)) {
791
842
  }
792
843
  }
793
844
  if (action === "safety-pre") {
794
- // A malformed or unavailable safety check must not turn into permission.
795
845
  let payload;
796
846
  try {
797
847
  payload = JSON.parse(await stdinText());
@@ -803,6 +853,9 @@ export async function main(argv = process.argv.slice(2)) {
803
853
  else for (const nudge of nudges) console.log(nudge);
804
854
  return;
805
855
  } catch (error) {
856
+ // Only an evaluated safety rule may deny. Discovery, parsing, git,
857
+ // config and Dev errors fail open and produce no output.
858
+ if (!String(error?.code ?? "").startsWith("SAFETY_")) return;
806
859
  if (payload && hookRuntime(payload) === "codex") {
807
860
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: error?.message ?? String(error) } }));
808
861
  return;
@@ -821,14 +874,7 @@ export async function main(argv = process.argv.slice(2)) {
821
874
  payload = JSON.parse(await stdinText());
822
875
  await recordHookFire(payload).catch(() => {});
823
876
  await safetyPostHook(payload);
824
- } catch (error) {
825
- // A post-hook cannot undo a completed command; report state-tracking
826
- // failure without making Claude treat the completed command as failed.
827
- const message = `nodii-dev safety post hook: ${error?.code ?? "ERROR"}: ${error?.message ?? String(error)}`;
828
- if (payload && hookRuntime(payload) === "codex")
829
- console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: message } }));
830
- else console.error(message);
831
- }
877
+ } catch {}
832
878
  return;
833
879
  }
834
880
  if (action === "lane-heartbeat") {
@@ -854,6 +900,7 @@ export async function main(argv = process.argv.slice(2)) {
854
900
  return emit(await postHook(payload), json, "logged");
855
901
  } catch (error) {
856
902
  const outcome = postHookFailure(error);
903
+ if (!outcome.exitCode) return;
857
904
  if (hookRuntime(payload) === "codex")
858
905
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: outcome.message } }));
859
906
  else {
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.26";
7
+ export const VERSION = "0.6.28";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
@@ -199,12 +199,16 @@ 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", "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"]);
203
203
  export function parseArgs(argv) {
204
204
  const flags = {};
205
205
  const args = [];
206
206
  for (let i = 0; i < argv.length; i++) {
207
207
  const token = argv[i];
208
+ if (token === "--") {
209
+ args.push(...argv.slice(i + 1));
210
+ break;
211
+ }
208
212
  if (!token.startsWith("--")) {
209
213
  args.push(token);
210
214
  continue;