@nodii/dev 0.6.24 → 0.6.26

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
@@ -309,7 +309,8 @@ nodii-dev lane step KEY --side backend|frontend --start|--done|--block STEP [--r
309
309
  # --block --reason records the new reason (Dev issue 125 moves it onto the step)
310
310
  nodii-dev lane evidence KEY --pr|--commit|--figma|--deploy|--dev-check REF
311
311
  nodii-dev lane heartbeat KEY [--note TEXT]
312
- nodii-dev lane review KEY
312
+ nodii-dev lane review KEY --file REVIEW.json
313
+ nodii-dev lane reviews KEY
313
314
  nodii-dev lane land KEY
314
315
  nodii-dev lane verify KEY --dev-check REF
315
316
  nodii-dev lane list [--kind KIND] [--status STATUS] [--machine UUID]
@@ -350,19 +351,40 @@ nodii-dev docs upload PATH... --class ruling|conformance|editorial
350
351
  nodii-dev docs ratify --domain KEY [--domain KEY ...] --decision DECISION.json [--wait]
351
352
  nodii-dev docs parked [CODE] [--status pending|acknowledged]
352
353
 
353
- nodii-dev harness install [--include-drafts] [--adopt PATH,...] [--dry-run]
354
- nodii-dev harness update [--include-drafts] [--adopt PATH,...] [--dry-run]
354
+ nodii-dev harness install [--include-drafts] [--adopt PATH,...] [--dry-run] [--system]
355
+ nodii-dev harness update [--include-drafts] [--adopt PATH,...] [--dry-run] [--system]
355
356
  nodii-dev harness check [--json] # 0 current, 1 differs from Dev, 2 no harness
356
357
  nodii-dev harness update --check # the same check
357
358
  ```
358
359
 
359
- `harness install` and `harness update` merge the two safety hooks into
360
- `.claude/settings.json`; every installed hook invokes only `nodii-dev`. The
360
+ `harness install` and `harness update` merge the safety hooks into both
361
+ `.claude/settings.json` and `.codex/hooks.json`; every installed hook invokes
362
+ only `nodii-dev`. The
361
363
  pre-hook blocks edits outside a lane's optional `ownedPaths`, generated UI/API
362
364
  files, unsafe Git operations, stale or missing review receipts, lane identity
363
365
  changes, production-secret/test-auth bypasses, cleanup of Docker resources the
364
366
  run did not create, and deploy/apply commands without an exact grant. Allowed
365
- calls produce no output.
367
+ calls produce no output. Codex receives runtime-native `Bash` and `apply_patch`
368
+ matchers, JSON denials, and `additionalContext` nudges. A compact session start
369
+ prints the current lane Freeze again.
370
+
371
+ Hard blocks apply only after Dev proves the current agent, branch and worktree
372
+ belong to an active lane. Interactive or otherwise unproven sessions receive
373
+ nudges instead. Push is blocked only when that proven lane also exposes the
374
+ review-receipt flow; until then the hook says it cannot verify the review and
375
+ allows the command.
376
+
377
+ Codex trusts system-managed hooks without a per-hash prompt. `--system` writes
378
+ the repository harness normally and refreshes the user-owned dispatcher under
379
+ `~/.config/nodii-dev/codex-managed-hooks`, then only prints the one-time
380
+ `sudo install` commands an operator can review and run to place its stable
381
+ policy in `/etc/codex/requirements.toml`; nodii-dev never runs sudo itself.
382
+ That system layer covers both `CODEX_HOME=~/.codex` and
383
+ `CODEX_HOME=~/.codex-work`; later `--system` updates need no sudo. The
384
+ dispatcher records the absolute executable path so hook processes never
385
+ depend on `PATH`, and refreshes it on every update. Outside a lane, or when
386
+ that executable is missing or too old, it fails open without diagnostics;
387
+ `doctor` reports that fail-open state for each Codex profile.
366
388
 
367
389
  Until Dev exposes a lane's owned paths (AGENT-HARNESS-10), an edit with no
368
390
  ownership field emits a nudge instead of claiming it is safe or blocking an
@@ -602,11 +624,14 @@ OAuth login.
602
624
  `harness install` writes the repository's agent harness as Dev generates it
603
625
  from the ratified methodology: the `CLAUDE.md` and `AGENTS.md` pointer
604
626
  blocks, one pointer skill per methodology document, the Claude Code hooks
605
- merged into `.claude/settings.json`, and Dev's `.nodii/harness.json`. Every
627
+ merged into `.claude/settings.json`, their Codex equivalents merged into
628
+ `.codex/hooks.json`, and Dev's `.nodii/harness.json`. Every
606
629
  generated file carries the methodology version it came from. Nothing about
607
630
  the rules is authored locally. Run it in the service's own repository: it
608
- installs one service at a time, and there is no fleet mode. `--include-drafts`
609
- uses draft methodology while none is ratified.
631
+ installs one service at a time, and there is no fleet mode. While no
632
+ methodology is ratified, install and update automatically retry with the
633
+ latest draft versions and print the exact draft methodology and source
634
+ versions they used. `--include-drafts` requests that behavior explicitly.
610
635
 
611
636
  The CLI records what it wrote in `.nodii/harness-state.json` and never touches
612
637
  anything else. An existing file it did not write is refused, unless the
@@ -654,6 +679,8 @@ exits 0 only when every check is `OK`. `--json` returns the same checks as
654
679
  | `lane_limit` | The agent's active lanes against its lane limit. A full limit is `OK` when a lane on this branch holds the slot. |
655
680
  | `figma_file` | Only with `--figma-file KEY` (a key or figma.com link), or `NODII_DEV_FIGMA_FILE`: whether Dev has the brief's Figma file registered. `MISSING` names the `figma file add` command, since `figma lock` would fail with `DESIGN_FILE_NOT_FOUND`. |
656
681
  | `figma_scopes` | With `figma_file`, when the file is registered: the change set this agent holds in the file and its lock scopes (or the whole file), or that it holds none (DEC-0032). |
682
+ | `hooks_claude` | Whether Claude hooks are installed, trusted by that runtime, and have a private firing receipt. |
683
+ | `hooks_codex_personal`, `hooks_codex_work` | For `~/.codex` and `~/.codex-work`, whether Codex's own hook registry reports the hooks installed and trusted (including managed status), whether the managed absolute CLI is present and new enough, and whether that profile has fired one. |
657
684
 
658
685
  Doctor may refresh the access token, as any command does. The permission table
659
686
  is kept by hand from Dev's route policy until Dev's OpenAPI names each
@@ -670,9 +697,9 @@ serves for that step (see Rule delivery). Server refusals,
670
697
  including operator-only actions, workflow blocks, lane limits and Built items
671
698
  that are not on dev, retain Dev's error code/message and exit non-zero.
672
699
 
673
- `setup` installs the Claude Code workflow harness in the current repository.
700
+ `setup` installs the Claude Code and Codex workflow harnesses in the current repository.
674
701
  It writes `.claude/skills/nodii-build-workflow/SKILL.md`, merges SessionStart,
675
- Stop and Bash PostToolUse hooks into `.claude/settings.json`, and maintains one
702
+ Stop and tool hooks into `.claude/settings.json` and `.codex/hooks.json`, and maintains one
676
703
  marked section in an existing `CLAUDE.md` or `AGENTS.md` (creating `CLAUDE.md`
677
704
  when neither exists). Existing settings and hooks are preserved. The default
678
705
  is a read-only preview; `--yes` applies, and `--check` is suitable for drift
@@ -712,6 +739,14 @@ The command reaches Dev through the edge, so it needs the edge's route for
712
739
  `/devops/api/v1/lanes/:id/non-issue`; until that is on an environment, the
713
740
  command gets the edge's 404 there.
714
741
 
742
+ `lane review` reads one JSON object with the review's exact `origin/main`
743
+ commit in `base`, exact reviewed commit in `head`,
744
+ `iterationCount`, `exitReason`, `appliedPatches`, `unresolvedProposals`,
745
+ `reviewerErrors`, and `findings`. Each finding has `perspective` (`code` or
746
+ `rules`), `severity`, `summary`, `detail`, `disposition`, optional `path` and
747
+ `line`, and a required `clauseId` for rules findings. `lane reviews` reads the
748
+ append-only history, including whether each cited clause was a missed trigger.
749
+
715
750
  Heartbeat notes are optional and limited by Dev to 500 characters.
716
751
  Prioritize and deprioritize require a 1–500 character reason, which the CLI
717
752
  sends to Dev. `lane review` records the review gate.
package/dist/cli.js CHANGED
@@ -20,11 +20,12 @@ import {
20
20
  refuseFromLane,
21
21
  revokeOAuth,
22
22
  } from "./oauth.js";
23
- import { heartbeatCurrentLane, postHook, postHookFailure, preHook, settingsSnippet, workflowSuggestion } from "./hooks.js";
23
+ import { heartbeatCurrentLane, postHook, postHookFailure, preHook, sessionStartContext, settingsSnippet, workflowSuggestion } from "./hooks.js";
24
24
  import { setupCommand } from "./setup.js";
25
25
  import { harnessCommand } from "./harness.js";
26
- import { safetyPostHook, safetyPreHook } from "./safety.js";
26
+ import { hookRuntime, recordHookFire, safetyPostHook, safetyPreHook } from "./safety.js";
27
27
  import { doctorCommand } from "./doctor.js";
28
+ import { dispatchManagedHook } from "./managed-hooks.js";
28
29
  import {
29
30
  figmaChangeSetShow,
30
31
  figmaChangeSetText,
@@ -120,9 +121,10 @@ Usage: nodii-dev <command> [options] [--json]
120
121
  figma changeset show CHANGE_SET # items, variants, round, decision and comment (with the previous round's)
121
122
  figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
122
123
  # register a Figma file in Dev so it can be locked
123
- hook claude-figma-pre|claude-figma-post|safety-pre|safety-post|print-settings
124
+ hook claude-figma-pre|claude-figma-post|safety-pre|safety-post|session-start|managed-dispatch|print-settings
124
125
  setup [--yes|--check] # dry-run by default
125
- harness install|update [--include-drafts[=false]] [--adopt PATH,...] [--dry-run]
126
+ harness install|update [--include-drafts[=false]] [--adopt PATH,...] [--dry-run] [--system]
127
+ # --system stages managed hooks and only prints operator-reviewed sudo commands
126
128
  harness check # 0 current, 1 differs from Dev, 2 not installed
127
129
  harness update --check # the same check
128
130
  docs check|validate|upload <path...> --class ruling|conformance|editorial
@@ -139,7 +141,9 @@ Usage: nodii-dev <command> [options] [--json]
139
141
  lane step <key> --side SIDE --start|--done|--block <step> [--reason TEXT]
140
142
  # --block --reason on a blocked step records its new reason
141
143
  lane evidence <key> --pr|--commit|--figma|--deploy|--dev-check REF
142
- lane heartbeat|review|land|verify|prioritize|deprioritize|stop <key>
144
+ lane heartbeat|land|verify|prioritize|deprioritize|stop <key>
145
+ lane review <key> --file REVIEW.json # base/head, outcome and clause-cited findings
146
+ lane reviews <key> # stored review history for the lane
143
147
  lane non-issue <key> --reason TEXT --evidence TEXT --reroute KEY[,KEY]|none
144
148
  --branch carried:KEY|dropped[:REASON] [--commits SHA[,SHA]]
145
149
  # DEC-0011: terminal non_issue; issue -> wont_do; slot frees
@@ -755,15 +759,29 @@ export async function main(argv = process.argv.slice(2)) {
755
759
  if (command === "hook") {
756
760
  if (action === "print-settings")
757
761
  return console.log(JSON.stringify(settingsSnippet(), null, 2));
758
- if (action === "claude-figma-pre") {
762
+ if (action === "managed-dispatch") {
763
+ if (!rest[0] || rest.length > 1) throw new CliError("USAGE", "hook managed-dispatch needs one event name", 2);
764
+ const output = await dispatchManagedHook(rest[0], await stdinText());
765
+ if (output) process.stdout.write(output);
766
+ return;
767
+ }
768
+ if (["claude-figma-pre", "figma-pre"].includes(action)) {
759
769
  // Fail closed: Claude Code only blocks a tool call on exit code 2, and
760
770
  // treats any other non-zero exit as a non-blocking error that lets the
761
771
  // write through. Every failure here (bad stdin, missing config, Dev
762
772
  // error, lock denied) must therefore exit 2.
773
+ let payload;
763
774
  try {
764
- const payload = JSON.parse(await stdinText());
765
- return emit(await preHook(payload), json, "allowed");
775
+ payload = JSON.parse(await stdinText());
776
+ await recordHookFire(payload).catch(() => {});
777
+ const result = await preHook(payload);
778
+ if (hookRuntime(payload) !== "codex") return emit(result, json, "allowed");
779
+ return;
766
780
  } catch (error) {
781
+ if (payload && hookRuntime(payload) === "codex") {
782
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: `Blocked Figma write: ${error?.message ?? String(error)}` } }));
783
+ return;
784
+ }
767
785
  throw new CliError(
768
786
  error?.code ?? "FIGMA_HOOK_FAILED",
769
787
  `Blocked Figma write: ${error?.message ?? String(error)}`,
@@ -774,11 +792,21 @@ export async function main(argv = process.argv.slice(2)) {
774
792
  }
775
793
  if (action === "safety-pre") {
776
794
  // A malformed or unavailable safety check must not turn into permission.
795
+ let payload;
777
796
  try {
778
- const result = await safetyPreHook(JSON.parse(await stdinText()));
779
- for (const nudge of result.nudges ?? []) console.log(nudge);
797
+ payload = JSON.parse(await stdinText());
798
+ await recordHookFire(payload).catch(() => {});
799
+ const result = await safetyPreHook(payload);
800
+ const nudges = result.nudges ?? [];
801
+ if (nudges.length && hookRuntime(payload) === "codex")
802
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: nudges.join("\n") } }));
803
+ else for (const nudge of nudges) console.log(nudge);
780
804
  return;
781
805
  } catch (error) {
806
+ if (payload && hookRuntime(payload) === "codex") {
807
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: error?.message ?? String(error) } }));
808
+ return;
809
+ }
782
810
  throw new CliError(
783
811
  error?.code ?? "SAFETY_HOOK_FAILED",
784
812
  error?.message ?? String(error),
@@ -788,12 +816,18 @@ export async function main(argv = process.argv.slice(2)) {
788
816
  }
789
817
  }
790
818
  if (action === "safety-post") {
819
+ let payload;
791
820
  try {
792
- await safetyPostHook(JSON.parse(await stdinText()));
821
+ payload = JSON.parse(await stdinText());
822
+ await recordHookFire(payload).catch(() => {});
823
+ await safetyPostHook(payload);
793
824
  } catch (error) {
794
825
  // A post-hook cannot undo a completed command; report state-tracking
795
826
  // failure without making Claude treat the completed command as failed.
796
- console.error(`nodii-dev safety post hook: ${error?.code ?? "ERROR"}: ${error?.message ?? String(error)}`);
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);
797
831
  }
798
832
  return;
799
833
  }
@@ -802,18 +836,30 @@ export async function main(argv = process.argv.slice(2)) {
802
836
  return emit(result, json, result.heartbeated ? `Heartbeat recorded for ${result.key}.` : "No current-branch lane; no heartbeat sent.");
803
837
  }
804
838
  const payload = JSON.parse(await stdinText());
839
+ await recordHookFire(payload).catch(() => {});
840
+ if (action === "session-start") {
841
+ const context = await sessionStartContext(payload);
842
+ if (context) console.log(context);
843
+ return;
844
+ }
805
845
  if (action === "workflow-post") {
806
846
  const suggestion = workflowSuggestion(payload);
807
- if (suggestion) console.log(suggestion);
847
+ if (suggestion && hookRuntime(payload) === "codex")
848
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: suggestion } }));
849
+ else if (suggestion) console.log(suggestion);
808
850
  return;
809
851
  }
810
- if (action === "claude-figma-post") {
852
+ if (["claude-figma-post", "figma-post"].includes(action)) {
811
853
  try {
812
854
  return emit(await postHook(payload), json, "logged");
813
855
  } catch (error) {
814
856
  const outcome = postHookFailure(error);
815
- console.error(outcome.message);
816
- if (outcome.exitCode) process.exitCode = outcome.exitCode;
857
+ if (hookRuntime(payload) === "codex")
858
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: outcome.message } }));
859
+ else {
860
+ console.error(outcome.message);
861
+ if (outcome.exitCode) process.exitCode = outcome.exitCode;
862
+ }
817
863
  return;
818
864
  }
819
865
  }
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.24";
7
+ export const VERSION = "0.6.26";
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", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted", "library", "async", "sync", "force"]);
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"]);
203
203
  export function parseArgs(argv) {
204
204
  const flags = {};
205
205
  const args = [];