oh-my-codex 0.19.1 → 0.20.1

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.
Files changed (229) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -1
  4. package/crates/omx-api/src/lib.rs +3 -3
  5. package/crates/omx-api/tests/cli.rs +4 -4
  6. package/crates/omx-explore/src/main.rs +4 -4
  7. package/crates/omx-sparkshell/src/codex_bridge.rs +2 -2
  8. package/dist/agents/__tests__/definitions.test.js +3 -3
  9. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  10. package/dist/agents/__tests__/native-config.test.js +71 -42
  11. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  12. package/dist/agents/definitions.d.ts +1 -1
  13. package/dist/agents/definitions.d.ts.map +1 -1
  14. package/dist/agents/definitions.js +3 -3
  15. package/dist/agents/definitions.js.map +1 -1
  16. package/dist/agents/native-config.d.ts +2 -2
  17. package/dist/agents/native-config.d.ts.map +1 -1
  18. package/dist/agents/native-config.js +7 -5
  19. package/dist/agents/native-config.js.map +1 -1
  20. package/dist/autopilot/__tests__/planner-routing.test.js +19 -12
  21. package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -1
  22. package/dist/autopilot/__tests__/ralplan-gate.test.js +151 -0
  23. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  24. package/dist/autopilot/planner-routing.d.ts.map +1 -1
  25. package/dist/autopilot/planner-routing.js +4 -2
  26. package/dist/autopilot/planner-routing.js.map +1 -1
  27. package/dist/autopilot/ralplan-gate.d.ts +1 -0
  28. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  29. package/dist/autopilot/ralplan-gate.js +38 -2
  30. package/dist/autopilot/ralplan-gate.js.map +1 -1
  31. package/dist/autoresearch/runtime.d.ts +2 -0
  32. package/dist/autoresearch/runtime.d.ts.map +1 -1
  33. package/dist/autoresearch/runtime.js +8 -0
  34. package/dist/autoresearch/runtime.js.map +1 -1
  35. package/dist/capabilities/__tests__/lockfile.test.d.ts +2 -0
  36. package/dist/capabilities/__tests__/lockfile.test.d.ts.map +1 -0
  37. package/dist/capabilities/__tests__/lockfile.test.js +115 -0
  38. package/dist/capabilities/__tests__/lockfile.test.js.map +1 -0
  39. package/dist/capabilities/lockfile.d.ts +102 -0
  40. package/dist/capabilities/lockfile.d.ts.map +1 -0
  41. package/dist/capabilities/lockfile.js +334 -0
  42. package/dist/capabilities/lockfile.js.map +1 -0
  43. package/dist/cli/__tests__/agents.test.js +5 -5
  44. package/dist/cli/__tests__/agents.test.js.map +1 -1
  45. package/dist/cli/__tests__/capabilities.test.d.ts +2 -0
  46. package/dist/cli/__tests__/capabilities.test.d.ts.map +1 -0
  47. package/dist/cli/__tests__/capabilities.test.js +135 -0
  48. package/dist/cli/__tests__/capabilities.test.js.map +1 -0
  49. package/dist/cli/__tests__/codex-plugin-layout.test.js +264 -6
  50. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  51. package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
  52. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  53. package/dist/cli/__tests__/doctor-invalid-config.test.js +10 -1
  54. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  55. package/dist/cli/__tests__/doctor-spark-routing.test.js +22 -6
  56. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
  57. package/dist/cli/__tests__/doctor-warning-copy.test.js +87 -1
  58. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  59. package/dist/cli/__tests__/index.test.js +53 -18
  60. package/dist/cli/__tests__/index.test.js.map +1 -1
  61. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +3 -3
  62. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -1
  63. package/dist/cli/__tests__/resume.test.js +95 -6
  64. package/dist/cli/__tests__/resume.test.js.map +1 -1
  65. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +54 -2
  66. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  67. package/dist/cli/__tests__/setup-install-mode.test.js +49 -21
  68. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  69. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +3 -3
  70. package/dist/cli/__tests__/setup-refresh.test.js +166 -12
  71. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  72. package/dist/cli/__tests__/setup-scope.test.js +27 -3
  73. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  74. package/dist/cli/__tests__/uninstall.test.js +301 -7
  75. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  76. package/dist/cli/agents.js +1 -1
  77. package/dist/cli/agents.js.map +1 -1
  78. package/dist/cli/capabilities.d.ts +3 -0
  79. package/dist/cli/capabilities.d.ts.map +1 -0
  80. package/dist/cli/capabilities.js +118 -0
  81. package/dist/cli/capabilities.js.map +1 -0
  82. package/dist/cli/doctor.d.ts +3 -1
  83. package/dist/cli/doctor.d.ts.map +1 -1
  84. package/dist/cli/doctor.js +50 -34
  85. package/dist/cli/doctor.js.map +1 -1
  86. package/dist/cli/index.d.ts +7 -4
  87. package/dist/cli/index.d.ts.map +1 -1
  88. package/dist/cli/index.js +77 -19
  89. package/dist/cli/index.js.map +1 -1
  90. package/dist/cli/plugin-marketplace.d.ts +1 -0
  91. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  92. package/dist/cli/plugin-marketplace.js +31 -0
  93. package/dist/cli/plugin-marketplace.js.map +1 -1
  94. package/dist/cli/setup.d.ts.map +1 -1
  95. package/dist/cli/setup.js +9 -6
  96. package/dist/cli/setup.js.map +1 -1
  97. package/dist/cli/uninstall.d.ts.map +1 -1
  98. package/dist/cli/uninstall.js +3 -4
  99. package/dist/cli/uninstall.js.map +1 -1
  100. package/dist/config/__tests__/generator-idempotent.test.js +210 -114
  101. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  102. package/dist/config/__tests__/generator-notify.test.js +26 -52
  103. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  104. package/dist/config/__tests__/models.test.js +51 -26
  105. package/dist/config/__tests__/models.test.js.map +1 -1
  106. package/dist/config/generator.d.ts +20 -11
  107. package/dist/config/generator.d.ts.map +1 -1
  108. package/dist/config/generator.js +169 -152
  109. package/dist/config/generator.js.map +1 -1
  110. package/dist/config/models.d.ts +16 -6
  111. package/dist/config/models.d.ts.map +1 -1
  112. package/dist/config/models.js +19 -8
  113. package/dist/config/models.js.map +1 -1
  114. package/dist/hooks/__tests__/keyword-detector.test.js +12 -4
  115. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  116. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +1 -1
  117. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +2 -2
  118. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  119. package/dist/hooks/__tests__/tmux-hook-engine.test.js +2 -2
  120. package/dist/leader/__tests__/contract.test.js +150 -1
  121. package/dist/leader/__tests__/contract.test.js.map +1 -1
  122. package/dist/leader/contract.d.ts +26 -0
  123. package/dist/leader/contract.d.ts.map +1 -1
  124. package/dist/leader/contract.js +188 -0
  125. package/dist/leader/contract.js.map +1 -1
  126. package/dist/modes/base.d.ts +1 -1
  127. package/dist/modes/base.d.ts.map +1 -1
  128. package/dist/pipeline/__tests__/stages.test.js +27 -1
  129. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  130. package/dist/pipeline/stages/ralplan.d.ts +2 -1
  131. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  132. package/dist/pipeline/stages/ralplan.js +1 -0
  133. package/dist/pipeline/stages/ralplan.js.map +1 -1
  134. package/dist/pipeline/stages/ultragoal.d.ts +5 -1
  135. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -1
  136. package/dist/pipeline/stages/ultragoal.js +14 -4
  137. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  138. package/dist/ralplan/__tests__/runtime.test.js +132 -0
  139. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  140. package/dist/ralplan/runtime.d.ts +17 -0
  141. package/dist/ralplan/runtime.d.ts.map +1 -1
  142. package/dist/ralplan/runtime.js +97 -1
  143. package/dist/ralplan/runtime.js.map +1 -1
  144. package/dist/scripts/__tests__/codex-native-hook.test.js +1528 -32
  145. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  146. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  147. package/dist/scripts/codex-native-hook.js +377 -31
  148. package/dist/scripts/codex-native-hook.js.map +1 -1
  149. package/dist/state/__tests__/operations.test.js +450 -0
  150. package/dist/state/__tests__/operations.test.js.map +1 -1
  151. package/dist/state/__tests__/workflow-transition.test.js +14 -0
  152. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  153. package/dist/state/operations.d.ts.map +1 -1
  154. package/dist/state/operations.js +72 -11
  155. package/dist/state/operations.js.map +1 -1
  156. package/dist/state/terminal-normalization.d.ts +8 -0
  157. package/dist/state/terminal-normalization.d.ts.map +1 -0
  158. package/dist/state/terminal-normalization.js +127 -0
  159. package/dist/state/terminal-normalization.js.map +1 -0
  160. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  161. package/dist/state/workflow-transition-reconcile.js +3 -1
  162. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  163. package/dist/team/__tests__/delegation-policy.test.js +2 -2
  164. package/dist/team/__tests__/delegation-policy.test.js.map +1 -1
  165. package/dist/team/__tests__/model-contract.test.js +25 -25
  166. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  167. package/dist/team/__tests__/runtime.test.js +28 -28
  168. package/dist/team/__tests__/runtime.test.js.map +1 -1
  169. package/dist/team/__tests__/scaling.test.js +7 -7
  170. package/dist/team/__tests__/scaling.test.js.map +1 -1
  171. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +5 -5
  172. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -1
  173. package/dist/team/__tests__/tmux-session.test.js +12 -12
  174. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  175. package/dist/team/__tests__/worker-bootstrap.test.js +57 -9
  176. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  177. package/dist/team/__tests__/worker-runtime-identity.test.js +3 -3
  178. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  179. package/dist/team/model-contract.d.ts +1 -1
  180. package/dist/team/model-contract.d.ts.map +1 -1
  181. package/dist/team/runtime.d.ts.map +1 -1
  182. package/dist/team/runtime.js +11 -0
  183. package/dist/team/runtime.js.map +1 -1
  184. package/dist/team/worker-bootstrap.d.ts +2 -0
  185. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  186. package/dist/team/worker-bootstrap.js +6 -1
  187. package/dist/team/worker-bootstrap.js.map +1 -1
  188. package/dist/ultragoal/__tests__/artifacts.test.js +68 -1
  189. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  190. package/dist/ultragoal/artifacts.d.ts +5 -1
  191. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  192. package/dist/ultragoal/artifacts.js +41 -9
  193. package/dist/ultragoal/artifacts.js.map +1 -1
  194. package/dist/utils/__tests__/agents-md.test.js +16 -0
  195. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  196. package/dist/utils/__tests__/agents-model-table.test.js +7 -7
  197. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  198. package/dist/utils/__tests__/worktree-tool-context.test.d.ts +2 -0
  199. package/dist/utils/__tests__/worktree-tool-context.test.d.ts.map +1 -0
  200. package/dist/utils/__tests__/worktree-tool-context.test.js +118 -0
  201. package/dist/utils/__tests__/worktree-tool-context.test.js.map +1 -0
  202. package/dist/utils/agents-md.d.ts.map +1 -1
  203. package/dist/utils/agents-md.js +4 -3
  204. package/dist/utils/agents-md.js.map +1 -1
  205. package/dist/utils/worktree-tool-context.d.ts +25 -0
  206. package/dist/utils/worktree-tool-context.d.ts.map +1 -0
  207. package/dist/utils/worktree-tool-context.js +116 -0
  208. package/dist/utils/worktree-tool-context.js.map +1 -0
  209. package/package.json +1 -1
  210. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  211. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +96 -10
  212. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +1 -1
  213. package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -1
  214. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +5 -5
  215. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +3 -3
  216. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  217. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  218. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +2 -2
  219. package/prompts/prometheus-strict-metis.md +1 -1
  220. package/skills/ai-slop-cleaner/SKILL.md +1 -1
  221. package/skills/code-review/SKILL.md +1 -1
  222. package/skills/ecomode/references/agent-tiers.md +1 -1
  223. package/skills/omx-setup/SKILL.md +5 -5
  224. package/skills/prometheus-strict/SKILL.md +3 -3
  225. package/skills/ralplan/SKILL.md +1 -1
  226. package/skills/team/SKILL.md +1 -1
  227. package/skills/ultrawork/references/agent-tiers.md +2 -2
  228. package/src/scripts/__tests__/codex-native-hook.test.ts +1818 -113
  229. package/src/scripts/codex-native-hook.ts +414 -27
@@ -782,6 +782,72 @@ describe("codex native hook dispatch", () => {
782
782
  }
783
783
  });
784
784
 
785
+ it("allows Ralplan Markdown draft-only apply_patch on the live CLI path while denying mixed targets", async () => {
786
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-draft-boundary-"));
787
+ const sessionId = "sess-cli-ralplan-draft-boundary";
788
+ const stateDir = join(cwd, ".omx", "state");
789
+ try {
790
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
791
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
792
+ active: true,
793
+ skill: "ralplan",
794
+ phase: "planning",
795
+ session_id: sessionId,
796
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
797
+ });
798
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
799
+ active: true,
800
+ mode: "ralplan",
801
+ current_phase: "planning",
802
+ session_id: sessionId,
803
+ });
804
+
805
+ for (const [name, target] of [
806
+ ["relative", ".omx/drafts/issue-3105.md"],
807
+ ["repository-absolute", join(cwd, ".omx", "drafts", "issue-3105.md")],
808
+ ] as const) {
809
+ const result = runNativeHookCliResult({
810
+ hook_event_name: "PreToolUse",
811
+ cwd,
812
+ session_id: sessionId,
813
+ thread_id: `thread-cli-ralplan-draft-${name}`,
814
+ tool_name: "apply_patch",
815
+ tool_input: {
816
+ input: `*** Begin Patch\n*** Add File: ${target}\n+# Draft\n*** End Patch\n`,
817
+ },
818
+ }, { cwd });
819
+
820
+ assert.equal(result.status, 0, result.stderr || result.stdout);
821
+ assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
822
+ }
823
+
824
+ const mixedResult = runNativeHookCliResult({
825
+ hook_event_name: "PreToolUse",
826
+ cwd,
827
+ session_id: sessionId,
828
+ thread_id: "thread-cli-ralplan-draft-mixed",
829
+ tool_name: "apply_patch",
830
+ tool_input: {
831
+ input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
832
+ },
833
+ }, { cwd });
834
+
835
+ assert.equal(mixedResult.status, 0, mixedResult.stderr || mixedResult.stdout);
836
+ const mixedOutput = parseSingleJsonStdout(mixedResult.stdout);
837
+ const hookSpecificOutput = mixedOutput.hookSpecificOutput as Record<string, unknown>;
838
+ assert.deepEqual(Object.keys(mixedOutput).sort(), ["hookSpecificOutput"]);
839
+ assert.equal(hookSpecificOutput.hookEventName, "PreToolUse");
840
+ assert.equal(hookSpecificOutput.permissionDecision, "deny");
841
+ assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /src\/leak\.ts/);
842
+ assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /implementation\/write tools are blocked/);
843
+ assert.equal(mixedOutput.decision, undefined);
844
+ assert.equal(mixedOutput.reason, undefined);
845
+ assert.equal(mixedOutput.systemMessage, undefined);
846
+ } finally {
847
+ await rm(cwd, { recursive: true, force: true });
848
+ }
849
+ });
850
+
785
851
  it("preserves team-worker typed subagent PreToolUse exemption without thread spawn provenance", async () => {
786
852
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-worker-typed-pretool-exempt-"));
787
853
  const sessionId = "sess-team-worker-typed-pretool-exempt";
@@ -1518,13 +1584,56 @@ PY`,
1518
1584
  cwd,
1519
1585
  session_id: sessionId,
1520
1586
  thread_id: "thread-autopilot-diagnostic",
1521
- }, { cwd })) as { decision?: string; reason?: string; statePath?: string; canonicalDisagreement?: string };
1587
+ }, { cwd }));
1522
1588
 
1523
1589
  assert.equal(output.decision, "block");
1590
+ assert.equal(output.stopReason, "autopilot_ultragoal");
1524
1591
  assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
1525
1592
  assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
1526
- assert.equal(output.statePath, ".omx/state/sessions/sess-autopilot-diagnostic/autopilot-state.json");
1527
- assert.equal(output.canonicalDisagreement, "canonical_agrees");
1593
+ assert.match(String(output.systemMessage ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
1594
+ assert.match(String(output.systemMessage ?? ""), /canonical: canonical_agrees/);
1595
+ assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
1596
+ assert.equal("statePath" in output, false);
1597
+ assert.equal("canonicalDisagreement" in output, false);
1598
+ } finally {
1599
+ await rm(cwd, { recursive: true, force: true });
1600
+ }
1601
+ });
1602
+
1603
+ it("keeps canonical phase disagreements in active Autopilot Stop diagnostics", async () => {
1604
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-canonical-phase-"));
1605
+ try {
1606
+ const sessionId = "sess-autopilot-canonical-phase";
1607
+ await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
1608
+ await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
1609
+ version: 1,
1610
+ active: true,
1611
+ skill: "autopilot",
1612
+ phase: "execution",
1613
+ session_id: sessionId,
1614
+ active_skills: [{ skill: "autopilot", phase: "execution", active: true, session_id: sessionId }],
1615
+ });
1616
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
1617
+ active: true,
1618
+ mode: "autopilot",
1619
+ current_phase: "ultragoal",
1620
+ session_id: sessionId,
1621
+ });
1622
+
1623
+ const output = parseSingleJsonStdout(runNativeHookCli({
1624
+ hook_event_name: "Stop",
1625
+ cwd,
1626
+ session_id: sessionId,
1627
+ thread_id: "thread-autopilot-canonical-phase",
1628
+ }, { cwd }));
1629
+
1630
+ assert.equal(output.decision, "block");
1631
+ assert.equal(output.stopReason, "autopilot_ultragoal");
1632
+ assert.match(String(output.reason ?? ""), /canonical: canonical_phase:execution/);
1633
+ assert.match(String(output.systemMessage ?? ""), /canonical: canonical_phase:execution/);
1634
+ assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
1635
+ assert.equal("statePath" in output, false);
1636
+ assert.equal("canonicalDisagreement" in output, false);
1528
1637
  } finally {
1529
1638
  await rm(cwd, { recursive: true, force: true });
1530
1639
  }
@@ -1819,6 +1928,193 @@ PY`,
1819
1928
  }
1820
1929
  });
1821
1930
 
1931
+ it("adds resume-by-id instructions for persisted subagents on SessionStart resume", async () => {
1932
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-reopen-"));
1933
+ try {
1934
+ const stateDir = join(cwd, ".omx", "state");
1935
+ const sessionId = "omx-reopen-session";
1936
+ await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-reopen", pid: process.pid });
1937
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
1938
+ schemaVersion: 1,
1939
+ sessions: {
1940
+ [sessionId]: {
1941
+ session_id: sessionId,
1942
+ leader_thread_id: "codex-leader-reopen",
1943
+ updated_at: "2026-07-09T00:00:00.000Z",
1944
+ threads: {
1945
+ "codex-leader-reopen": {
1946
+ thread_id: "codex-leader-reopen",
1947
+ kind: "leader",
1948
+ first_seen_at: "2026-07-09T00:00:00.000Z",
1949
+ last_seen_at: "2026-07-09T00:00:00.000Z",
1950
+ turn_count: 1,
1951
+ },
1952
+ "thread-architect-reopen": {
1953
+ thread_id: "thread-architect-reopen",
1954
+ kind: "subagent",
1955
+ first_seen_at: "2026-07-09T00:01:00.000Z",
1956
+ last_seen_at: "2026-07-09T00:01:00.000Z",
1957
+ turn_count: 2,
1958
+ role: "architect",
1959
+ lane_id: "plan-review",
1960
+ scope: "SessionStart reopen",
1961
+ status: "available",
1962
+ last_handoff_summary: "reviewed the restart plan",
1963
+ },
1964
+ },
1965
+ },
1966
+ },
1967
+ });
1968
+
1969
+ const result = await dispatchCodexNativeHook(
1970
+ {
1971
+ hook_event_name: "SessionStart",
1972
+ cwd,
1973
+ session_id: "codex-leader-reopen",
1974
+ source: "resume",
1975
+ },
1976
+ { cwd, sessionOwnerPid: process.pid },
1977
+ );
1978
+
1979
+ const additionalContext = String(
1980
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
1981
+ );
1982
+ assert.match(additionalContext, /\[Persisted subagent reopen\]/);
1983
+ assert.match(additionalContext, /resume_agent\("thread-architect-reopen"\)/);
1984
+ assert.match(additionalContext, /role: architect; lane: plan-review; scope: SessionStart reopen; status: available/);
1985
+ assert.match(additionalContext, /saved subagent ids found: 1/);
1986
+
1987
+ const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8")) as {
1988
+ sessions?: Record<string, { threads?: Record<string, { resume_requested_at?: string }> }>;
1989
+ };
1990
+ assert.match(
1991
+ tracking.sessions?.[sessionId]?.threads?.["thread-architect-reopen"]?.resume_requested_at ?? "",
1992
+ /^\d{4}-\d{2}-\d{2}T/,
1993
+ );
1994
+ } finally {
1995
+ await rm(cwd, { recursive: true, force: true });
1996
+ }
1997
+ });
1998
+
1999
+ it("does not suggest duplicate same-role subagent spawns when reopen ids exist", async () => {
2000
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-no-duplicates-"));
2001
+ try {
2002
+ const stateDir = join(cwd, ".omx", "state");
2003
+ const sessionId = "omx-reuse-session";
2004
+ await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-reuse", pid: process.pid });
2005
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2006
+ schemaVersion: 1,
2007
+ sessions: {
2008
+ [sessionId]: {
2009
+ session_id: sessionId,
2010
+ leader_thread_id: "codex-leader-reuse",
2011
+ updated_at: "2026-07-09T00:00:00.000Z",
2012
+ threads: {
2013
+ "codex-leader-reuse": {
2014
+ thread_id: "codex-leader-reuse",
2015
+ kind: "leader",
2016
+ first_seen_at: "2026-07-09T00:00:00.000Z",
2017
+ last_seen_at: "2026-07-09T00:00:00.000Z",
2018
+ turn_count: 1,
2019
+ },
2020
+ "thread-critic-reuse": {
2021
+ thread_id: "thread-critic-reuse",
2022
+ kind: "subagent",
2023
+ first_seen_at: "2026-07-09T00:01:00.000Z",
2024
+ last_seen_at: "2026-07-09T00:01:00.000Z",
2025
+ turn_count: 1,
2026
+ role: "critic",
2027
+ lane_id: "risk-review",
2028
+ status: "closed",
2029
+ },
2030
+ },
2031
+ },
2032
+ },
2033
+ });
2034
+
2035
+ const result = await dispatchCodexNativeHook(
2036
+ {
2037
+ hook_event_name: "SessionStart",
2038
+ cwd,
2039
+ session_id: "codex-leader-reuse",
2040
+ source: "startup",
2041
+ },
2042
+ { cwd, sessionOwnerPid: process.pid },
2043
+ );
2044
+
2045
+ const additionalContext = String(
2046
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
2047
+ );
2048
+ assert.match(additionalContext, /resume_agent\("thread-critic-reuse"\)/);
2049
+ assert.match(additionalContext, /avoid duplicate same-type subagent spawns/);
2050
+ assert.match(additionalContext, /do not spawn a new agent solely because reopen failed/);
2051
+ assert.doesNotMatch(additionalContext, /spawn.*critic.*replacement/i);
2052
+ } finally {
2053
+ await rm(cwd, { recursive: true, force: true });
2054
+ }
2055
+ });
2056
+
2057
+ it("surfaces clear warnings for unavailable persisted subagents without spawning replacements", async () => {
2058
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-warning-"));
2059
+ try {
2060
+ const stateDir = join(cwd, ".omx", "state");
2061
+ const sessionId = "omx-warning-session";
2062
+ await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-warning", pid: process.pid });
2063
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2064
+ schemaVersion: 1,
2065
+ sessions: {
2066
+ [sessionId]: {
2067
+ session_id: sessionId,
2068
+ leader_thread_id: "codex-leader-warning",
2069
+ updated_at: "2026-07-09T00:00:00.000Z",
2070
+ threads: {
2071
+ "codex-leader-warning": {
2072
+ thread_id: "codex-leader-warning",
2073
+ kind: "leader",
2074
+ first_seen_at: "2026-07-09T00:00:00.000Z",
2075
+ last_seen_at: "2026-07-09T00:00:00.000Z",
2076
+ turn_count: 1,
2077
+ },
2078
+ "thread-executor-unavailable": {
2079
+ thread_id: "thread-executor-unavailable",
2080
+ kind: "subagent",
2081
+ first_seen_at: "2026-07-09T00:01:00.000Z",
2082
+ last_seen_at: "2026-07-09T00:01:00.000Z",
2083
+ turn_count: 1,
2084
+ role: "executor",
2085
+ lane_id: "implementation",
2086
+ status: "unavailable",
2087
+ resume_failed_at: "2026-07-09T00:02:00.000Z",
2088
+ resume_failure_reason: "Codex reported missing thread id",
2089
+ },
2090
+ },
2091
+ },
2092
+ },
2093
+ });
2094
+
2095
+ const result = await dispatchCodexNativeHook(
2096
+ {
2097
+ hook_event_name: "SessionStart",
2098
+ cwd,
2099
+ session_id: "codex-leader-warning",
2100
+ source: "resume",
2101
+ },
2102
+ { cwd, sessionOwnerPid: process.pid },
2103
+ );
2104
+
2105
+ const additionalContext = String(
2106
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
2107
+ );
2108
+ assert.match(additionalContext, /No compatible saved subagent id is currently marked reopenable/);
2109
+ assert.match(additionalContext, /Reopen warnings:/);
2110
+ assert.match(additionalContext, /thread-executor-unavailable/);
2111
+ assert.match(additionalContext, /last failure: Codex reported missing thread id/);
2112
+ assert.doesNotMatch(additionalContext, /resume_agent\("thread-executor-unavailable"\)/);
2113
+ } finally {
2114
+ await rm(cwd, { recursive: true, force: true });
2115
+ }
2116
+ });
2117
+
1822
2118
  it("preserves canonical OMX session scope when native SessionStart arrives with a different id", async () => {
1823
2119
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-reconcile-"));
1824
2120
  try {
@@ -5039,6 +5335,82 @@ standardMaxRounds = 15
5039
5335
  }
5040
5336
  });
5041
5337
 
5338
+ it("keeps conductor guidance on autopilot activation after capacity-only native subagent evidence", async () => {
5339
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-capacity-native-"));
5340
+ try {
5341
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5342
+ await dispatchCodexNativeHook(
5343
+ {
5344
+ hook_event_name: "PostToolUse",
5345
+ cwd,
5346
+ session_id: "sess-autopilot-capacity-native",
5347
+ thread_id: "thread-autopilot-capacity-native",
5348
+ turn_id: "turn-autopilot-capacity-native-spawn",
5349
+ tool_name: "multi_agent_v1.spawn_agent",
5350
+ tool_response: { error: "collab spawn failed: agent thread limit reached" },
5351
+ },
5352
+ { cwd },
5353
+ );
5354
+
5355
+ const result = await dispatchCodexNativeHook(
5356
+ {
5357
+ hook_event_name: "UserPromptSubmit",
5358
+ cwd,
5359
+ session_id: "sess-autopilot-capacity-native",
5360
+ thread_id: "thread-autopilot-capacity-native",
5361
+ turn_id: "turn-autopilot-capacity-native-prompt",
5362
+ prompt: "$autopilot implement issue #3078",
5363
+ },
5364
+ { cwd },
5365
+ );
5366
+
5367
+ assert.equal(result.omxEventName, "keyword-detector");
5368
+ assert.equal(result.skillState?.skill, "autopilot");
5369
+ const message = String(
5370
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
5371
+ );
5372
+ assert.match(message, /Autopilot protocol:/);
5373
+ assert.match(message, /Conductor mode contract:/);
5374
+ assert.match(message, /Golden Rule: When the Main agent is acting in Conductor mode/);
5375
+ assert.match(message, /Conductor reuse and ledger guidance:/);
5376
+ assert.doesNotMatch(message, /Native subagent support is unavailable in this environment/);
5377
+ assert.doesNotMatch(message, /Reason: agent_thread_limit_reached/);
5378
+ } finally {
5379
+ await rm(cwd, { recursive: true, force: true });
5380
+ }
5381
+ });
5382
+
5383
+ it("omits conductor block and emits unsupported native guidance for unsupported autopilot first-run payload", async () => {
5384
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-unsupported-native-"));
5385
+ try {
5386
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5387
+ const result = await dispatchCodexNativeHook(
5388
+ {
5389
+ hook_event_name: "UserPromptSubmit",
5390
+ cwd,
5391
+ session_id: "sess-autopilot-unsupported-native",
5392
+ thread_id: "thread-autopilot-unsupported-native",
5393
+ turn_id: "turn-autopilot-unsupported-native",
5394
+ prompt: "$autopilot implement issue #3078",
5395
+ capabilities: { native_subagents: false, multi_agent_v1: false },
5396
+ },
5397
+ { cwd },
5398
+ );
5399
+
5400
+ const message = String(
5401
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
5402
+ );
5403
+ assert.match(message, /Autopilot protocol:/);
5404
+ assert.doesNotMatch(message, /Conductor mode contract:/);
5405
+ assert.doesNotMatch(message, /Conductor reuse and ledger guidance:/);
5406
+ assert.match(message, /Native subagent support is unavailable in this environment/);
5407
+ assert.match(message, /Reason: native_subagents_unsupported/);
5408
+ assert.match(message, /Do not call multi_agent_v1\.close_agent/);
5409
+ } finally {
5410
+ await rm(cwd, { recursive: true, force: true });
5411
+ }
5412
+ });
5413
+
5042
5414
  it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
5043
5415
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
5044
5416
  try {
@@ -7841,15 +8213,79 @@ exit 0
7841
8213
  );
7842
8214
  }
7843
8215
 
7844
- // Cross-mode non-terminal `omx state write` payloads are activations,
7845
- // because state_write normalizes them to active=true after the hook.
7846
- const blockedStateCliMutation = await preToolUse(
7847
- {
8216
+ const runtimeRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-runtime-root-"));
8217
+ const previousOmxRoot = process.env.OMX_ROOT;
8218
+ const runtimeSessionId = "sess-di-runtime-artifact";
8219
+ const runtimeStateDir = join(runtimeRoot, ".omx", "state");
8220
+ const runtimeSessionDir = join(runtimeStateDir, "sessions", runtimeSessionId);
8221
+ try {
8222
+ process.env.OMX_ROOT = runtimeRoot;
8223
+ await mkdir(runtimeSessionDir, { recursive: true });
8224
+ await writeJson(join(runtimeStateDir, "session.json"), { session_id: runtimeSessionId, cwd });
8225
+ await writeJson(join(runtimeSessionDir, "skill-active-state.json"), {
8226
+ version: 1,
8227
+ active: true,
8228
+ skill: "deep-interview",
8229
+ phase: "planning",
8230
+ session_id: runtimeSessionId,
8231
+ thread_id: threadId,
8232
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: runtimeSessionId, thread_id: threadId }],
8233
+ });
8234
+ await writeJson(join(runtimeSessionDir, "deep-interview-state.json"), {
8235
+ active: true,
8236
+ mode: "deep-interview",
8237
+ current_phase: "intent-first",
8238
+ session_id: runtimeSessionId,
8239
+ thread_id: threadId,
8240
+ });
8241
+
8242
+ const allowedRuntimeStateWrite = await dispatchCodexNativeHook({
7848
8243
  hook_event_name: "PreToolUse",
7849
8244
  cwd,
7850
- session_id: "sess-di-artifact",
7851
- tool_name: "Bash",
7852
- tool_use_id: "tool-di-state-cli-write",
8245
+ session_id: runtimeSessionId,
8246
+ thread_id: threadId,
8247
+ tool_name: "Write",
8248
+ tool_use_id: "tool-di-runtime-state-write",
8249
+ tool_input: { file_path: join(runtimeSessionDir, "deep-interview-state.json"), content: "{}\n" },
8250
+ }, { cwd });
8251
+ assert.equal(allowedRuntimeStateWrite.outputJson, null);
8252
+
8253
+ const blockedRuntimeStatePeerWrite = await dispatchCodexNativeHook({
8254
+ hook_event_name: "PreToolUse",
8255
+ cwd,
8256
+ session_id: runtimeSessionId,
8257
+ thread_id: threadId,
8258
+ tool_name: "Write",
8259
+ tool_use_id: "tool-di-runtime-peer-state-write",
8260
+ tool_input: { file_path: join(runtimeRoot, ".omx", "state", "sessions", "../outside", "deep-interview-state.json"), content: "{}\n" },
8261
+ }, { cwd });
8262
+ assert.equal((blockedRuntimeStatePeerWrite.outputJson as { decision?: string } | null)?.decision, "block");
8263
+
8264
+ const blockedRuntimeUnrelatedWrite = await dispatchCodexNativeHook({
8265
+ hook_event_name: "PreToolUse",
8266
+ cwd,
8267
+ session_id: runtimeSessionId,
8268
+ thread_id: threadId,
8269
+ tool_name: "Write",
8270
+ tool_use_id: "tool-di-runtime-unrelated-write",
8271
+ tool_input: { file_path: join(runtimeRoot, "unrelated", "deep-interview-state.json"), content: "{}\n" },
8272
+ }, { cwd });
8273
+ assert.equal((blockedRuntimeUnrelatedWrite.outputJson as { decision?: string } | null)?.decision, "block");
8274
+ } finally {
8275
+ if (typeof previousOmxRoot === "string") process.env.OMX_ROOT = previousOmxRoot;
8276
+ else delete process.env.OMX_ROOT;
8277
+ await rm(runtimeRoot, { recursive: true, force: true });
8278
+ }
8279
+
8280
+ // Cross-mode non-terminal `omx state write` payloads are activations,
8281
+ // because state_write normalizes them to active=true after the hook.
8282
+ const blockedStateCliMutation = await preToolUse(
8283
+ {
8284
+ hook_event_name: "PreToolUse",
8285
+ cwd,
8286
+ session_id: "sess-di-artifact",
8287
+ tool_name: "Bash",
8288
+ tool_use_id: "tool-di-state-cli-write",
7853
8289
  tool_input: { command: "omx state write --input '{\"mode\":\"ralph\",\"current_phase\":\"executing\"}' --json" },
7854
8290
  },
7855
8291
  { cwd },
@@ -10499,6 +10935,7 @@ exit 0
10499
10935
  for (const path of [
10500
10936
  ".omx/context/findings.md",
10501
10937
  ".omx/plans/issue-2863.md",
10938
+ ".omx/drafts/issue-3105.md",
10502
10939
  ".omx/specs/issue-2863.md",
10503
10940
  ".omx/state/required-planning-state.json",
10504
10941
  ]) {
@@ -10506,6 +10943,24 @@ exit 0
10506
10943
  assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
10507
10944
  }
10508
10945
 
10946
+ const allowedDraftEdit = await preToolUse("Edit", "tool-ralplan-draft-edit", {
10947
+ file_path: ".omx/drafts/issue-3105.md",
10948
+ old_string: "old",
10949
+ new_string: "new",
10950
+ });
10951
+ assert.equal(allowedDraftEdit.outputJson, null);
10952
+
10953
+ for (const path of [
10954
+ join(cwd, ".omx", "drafts", "issue-3105.md"),
10955
+ ".omx/drafts/subdir/../issue-3105.md",
10956
+ ]) {
10957
+ const normalizedDraftWrite = await preToolUse("Write", `tool-ralplan-normalized-draft-${path}`, {
10958
+ file_path: path,
10959
+ content: "# Draft",
10960
+ });
10961
+ assert.equal(normalizedDraftWrite.outputJson, null, `normalized draft path should be allowed: ${path}`);
10962
+ }
10963
+
10509
10964
  for (const protectedPath of [
10510
10965
  ".omx/state/sessions/sess-ralplan-guard/ralplan-state.json",
10511
10966
  ".omx/state/sessions/sess-ralplan-guard/autopilot-state.json",
@@ -10547,11 +11002,26 @@ exit 0
10547
11002
  });
10548
11003
  assert.equal(allowedPatchUpdate.outputJson, null);
10549
11004
 
11005
+ const allowedDraftPatchAdd = await preToolUse("apply_patch", "tool-ralplan-draft-patch-add", {
11006
+ input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** End Patch\n",
11007
+ });
11008
+ assert.equal(allowedDraftPatchAdd.outputJson, null);
11009
+
11010
+ const allowedDraftPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-draft-patch-update", {
11011
+ input: "*** Begin Patch\n*** Update File: .omx/drafts/issue-3105.md\n@@\n-old\n+new\n*** End Patch\n",
11012
+ });
11013
+ assert.equal(allowedDraftPatchUpdate.outputJson, null);
11014
+
10550
11015
  const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
10551
11016
  command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
10552
11017
  });
10553
11018
  assert.equal(allowedRedirect.outputJson, null);
10554
11019
 
11020
+ const allowedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-allow", {
11021
+ command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md",
11022
+ });
11023
+ assert.equal(allowedDraftRedirect.outputJson, null);
11024
+
10555
11025
  const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
10556
11026
  command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
10557
11027
  });
@@ -10613,6 +11083,41 @@ exit 0
10613
11083
  const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
10614
11084
  assert.match(editReason, /Edit path/);
10615
11085
  assert.match(editReason, /src\/implementation\.ts/);
11086
+ const blockedEditContext = String(
11087
+ (blockedEdit.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null)
11088
+ ?.hookSpecificOutput?.additionalContext ?? "",
11089
+ );
11090
+ assert.match(blockedEditContext, /Markdown drafts under `\.omx\/drafts\/\*\.md`/);
11091
+ for (const allowedCategory of [
11092
+ ".omx/context/",
11093
+ ".omx/plans/",
11094
+ ".omx/specs/",
11095
+ ".omx/tmp/",
11096
+ ".omx/state/",
11097
+ ".beads/",
11098
+ ]) {
11099
+ assert.match(blockedEditContext, new RegExp(allowedCategory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
11100
+ }
11101
+
11102
+ for (const blockedDraftPath of [
11103
+ ".omx/drafts/issue-3105.MD",
11104
+ ".omx/Drafts/issue-3105.md",
11105
+ ".omx/drafts/issue-3105",
11106
+ ".omx/drafts/run.sh",
11107
+ ".omx/drafts/subdir/issue-3105.md",
11108
+ ".omx/drafts/../../src/leak.ts",
11109
+ "/tmp/issue-3105.md",
11110
+ ]) {
11111
+ const blockedDraft = await preToolUse("Write", `tool-ralplan-draft-block-${blockedDraftPath}`, {
11112
+ file_path: blockedDraftPath,
11113
+ content: "bad",
11114
+ });
11115
+ assert.equal(
11116
+ (blockedDraft.outputJson as { decision?: string } | null)?.decision,
11117
+ "block",
11118
+ `draft path should be blocked: ${blockedDraftPath}`,
11119
+ );
11120
+ }
10616
11121
 
10617
11122
  const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
10618
11123
  input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
@@ -10622,6 +11127,18 @@ exit 0
10622
11127
  assert.match(mixedReason, /apply_patch target/);
10623
11128
  assert.match(mixedReason, /src\/leak\.ts/);
10624
11129
 
11130
+ const blockedMixedDraftPatch = await preToolUse("apply_patch", "tool-ralplan-draft-patch-mixed", {
11131
+ input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
11132
+ });
11133
+ assert.equal((blockedMixedDraftPatch.outputJson as { decision?: string } | null)?.decision, "block");
11134
+ assert.match(String((blockedMixedDraftPatch.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
11135
+
11136
+ const blockedMixedDraftBash = await preToolUse("Bash", "tool-ralplan-draft-bash-mixed", {
11137
+ command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md; printf 'bad\\n' > src/leak.ts",
11138
+ });
11139
+ assert.equal((blockedMixedDraftBash.outputJson as { decision?: string } | null)?.decision, "block");
11140
+ assert.match(String((blockedMixedDraftBash.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
11141
+
10625
11142
  const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
10626
11143
  input: "not a recognizable patch",
10627
11144
  });
@@ -10636,6 +11153,12 @@ exit 0
10636
11153
  assert.match(unresolvedReason, /unresolved Bash write target/);
10637
11154
  assert.match(unresolvedReason, /\$PLAN_PATH/);
10638
11155
 
11156
+ const blockedUnresolvedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-unresolved", {
11157
+ command: "cat > \"$DRAFT_PATH\" <<'EOF'\ncontent\nEOF",
11158
+ });
11159
+ assert.equal((blockedUnresolvedDraftRedirect.outputJson as { decision?: string } | null)?.decision, "block");
11160
+ assert.match(String((blockedUnresolvedDraftRedirect.outputJson as { reason?: string } | null)?.reason ?? ""), /\$DRAFT_PATH/);
11161
+
10639
11162
  const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
10640
11163
  file_path: ".omx/plans/../../src/leak.ts",
10641
11164
  content: "bad",
@@ -11291,6 +11814,175 @@ PY`,
11291
11814
  }
11292
11815
  });
11293
11816
 
11817
+ it("keeps untyped collaboration child implementation writes blocked under active ralplan planning while typed trusted agents pass (#3116)", async () => {
11818
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-ralplan-untyped-"));
11819
+ process.env.OMX_ROOT = cwd;
11820
+ try {
11821
+ const stateDir = join(cwd, ".omx", "state");
11822
+ const sessionId = "sess-3116-ralplan-untyped";
11823
+ const leaderThreadId = "thread-3116-ralplan-untyped-leader";
11824
+ const childThreadId = "thread-3116-ralplan-untyped-child";
11825
+ const sessionDir = join(stateDir, "sessions", sessionId);
11826
+ const nowIso = new Date().toISOString();
11827
+ await mkdir(sessionDir, { recursive: true });
11828
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
11829
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
11830
+ version: 1,
11831
+ active: true,
11832
+ skill: "autopilot",
11833
+ phase: "ralplan",
11834
+ session_id: sessionId,
11835
+ thread_id: leaderThreadId,
11836
+ active_skills: [
11837
+ { skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId, thread_id: leaderThreadId },
11838
+ ],
11839
+ });
11840
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
11841
+ active: true,
11842
+ mode: "autopilot",
11843
+ current_phase: "ralplan",
11844
+ session_id: sessionId,
11845
+ thread_id: leaderThreadId,
11846
+ });
11847
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
11848
+ schemaVersion: 1,
11849
+ sessions: {
11850
+ [sessionId]: {
11851
+ session_id: sessionId,
11852
+ leader_thread_id: leaderThreadId,
11853
+ updated_at: nowIso,
11854
+ threads: {
11855
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
11856
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
11857
+ },
11858
+ },
11859
+ },
11860
+ });
11861
+
11862
+ // Untyped tracked/spawn-provenance child must NOT bypass the ralplan planning guard.
11863
+ const untypedChild = await dispatchCodexNativeHook(
11864
+ {
11865
+ hook_event_name: "PreToolUse",
11866
+ cwd,
11867
+ session_id: sessionId,
11868
+ thread_id: childThreadId,
11869
+ agent_role: "collaboration-child",
11870
+ source: {
11871
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11872
+ },
11873
+ tool_name: "Edit",
11874
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11875
+ },
11876
+ { cwd },
11877
+ );
11878
+ assert.equal(untypedChild.outputJson?.decision, "block");
11879
+
11880
+ // A typed trusted agent retains its existing planning exemption.
11881
+ const typedChild = await dispatchCodexNativeHook(
11882
+ {
11883
+ hook_event_name: "PreToolUse",
11884
+ cwd,
11885
+ session_id: sessionId,
11886
+ thread_id: childThreadId,
11887
+ agent_role: "executor",
11888
+ source: {
11889
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11890
+ },
11891
+ tool_name: "Edit",
11892
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11893
+ },
11894
+ { cwd },
11895
+ );
11896
+ assert.equal(typedChild.outputJson, null);
11897
+ } finally {
11898
+ await rm(cwd, { recursive: true, force: true });
11899
+ }
11900
+ });
11901
+
11902
+ it("keeps untyped collaboration child implementation writes blocked under active deep-interview planning while typed trusted agents pass (#3116)", async () => {
11903
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-di-untyped-"));
11904
+ process.env.OMX_ROOT = cwd;
11905
+ try {
11906
+ const stateDir = join(cwd, ".omx", "state");
11907
+ const sessionId = "sess-3116-di-untyped";
11908
+ const leaderThreadId = "thread-3116-di-untyped-leader";
11909
+ const childThreadId = "thread-3116-di-untyped-child";
11910
+ const sessionDir = join(stateDir, "sessions", sessionId);
11911
+ const nowIso = new Date().toISOString();
11912
+ await mkdir(sessionDir, { recursive: true });
11913
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
11914
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
11915
+ version: 1,
11916
+ active: true,
11917
+ skill: "deep-interview",
11918
+ phase: "planning",
11919
+ session_id: sessionId,
11920
+ thread_id: leaderThreadId,
11921
+ active_skills: [
11922
+ { skill: "deep-interview", phase: "planning", active: true, session_id: sessionId, thread_id: leaderThreadId },
11923
+ ],
11924
+ });
11925
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
11926
+ active: true,
11927
+ mode: "deep-interview",
11928
+ current_phase: "intent-first",
11929
+ session_id: sessionId,
11930
+ });
11931
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
11932
+ schemaVersion: 1,
11933
+ sessions: {
11934
+ [sessionId]: {
11935
+ session_id: sessionId,
11936
+ leader_thread_id: leaderThreadId,
11937
+ updated_at: nowIso,
11938
+ threads: {
11939
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
11940
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
11941
+ },
11942
+ },
11943
+ },
11944
+ });
11945
+
11946
+ // Untyped tracked/spawn-provenance child must NOT bypass the deep-interview planning guard.
11947
+ const untypedChild = await dispatchCodexNativeHook(
11948
+ {
11949
+ hook_event_name: "PreToolUse",
11950
+ cwd,
11951
+ session_id: sessionId,
11952
+ thread_id: childThreadId,
11953
+ agent_role: "collaboration-child",
11954
+ source: {
11955
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11956
+ },
11957
+ tool_name: "Edit",
11958
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11959
+ },
11960
+ { cwd },
11961
+ );
11962
+ assert.equal(untypedChild.outputJson?.decision, "block");
11963
+
11964
+ // A typed trusted agent retains its existing planning exemption.
11965
+ const typedChild = await dispatchCodexNativeHook(
11966
+ {
11967
+ hook_event_name: "PreToolUse",
11968
+ cwd,
11969
+ session_id: sessionId,
11970
+ thread_id: childThreadId,
11971
+ agent_role: "executor",
11972
+ source: {
11973
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11974
+ },
11975
+ tool_name: "Edit",
11976
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11977
+ },
11978
+ { cwd },
11979
+ );
11980
+ assert.equal(typedChild.outputJson, null);
11981
+ } finally {
11982
+ await rm(cwd, { recursive: true, force: true });
11983
+ }
11984
+ });
11985
+
11294
11986
  it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
11295
11987
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
11296
11988
  try {
@@ -11490,6 +12182,38 @@ PY`,
11490
12182
  assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
11491
12183
  assert.match(String(blocker.error_summary), /agent thread limit reached/);
11492
12184
  assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
12185
+ assert.equal(existsSync(join(cwd, ".omx", "state", "native-subagent-support.json")), false);
12186
+ } finally {
12187
+ await rm(cwd, { recursive: true, force: true });
12188
+ }
12189
+ });
12190
+
12191
+ it("records unsupported native subagent support blocker from spawn_agent PostToolUse output", async () => {
12192
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-support-record-"));
12193
+ try {
12194
+ const result = await dispatchCodexNativeHook(
12195
+ {
12196
+ hook_event_name: "PostToolUse",
12197
+ cwd,
12198
+ session_id: "sess-subagent-support-record",
12199
+ thread_id: "thread-subagent-support-record",
12200
+ turn_id: "turn-subagent-support-record",
12201
+ tool_name: "multi_agent_v1.spawn_agent",
12202
+ tool_response: { error: "unknown tool: multi_agent_v1.spawn_agent is unavailable" },
12203
+ },
12204
+ { cwd },
12205
+ );
12206
+
12207
+ assert.equal(result.omxEventName, "post-tool-use");
12208
+ const blocker = JSON.parse(
12209
+ await readFile(join(cwd, ".omx", "state", "native-subagent-support.json"), "utf-8"),
12210
+ ) as Record<string, unknown>;
12211
+ assert.equal(blocker.status, "unsupported");
12212
+ assert.equal(blocker.reason, "multi_agent_v1_unavailable");
12213
+ assert.equal(blocker.session_id, "sess-subagent-support-record");
12214
+ assert.equal(blocker.thread_id, "thread-subagent-support-record");
12215
+ assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
12216
+ assert.match(String(blocker.evidence), /unknown tool/);
11493
12217
  } finally {
11494
12218
  await rm(cwd, { recursive: true, force: true });
11495
12219
  }
@@ -17875,21 +18599,20 @@ PY`,
17875
18599
  execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
17876
18600
  await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
17877
18601
 
17878
- const result = await dispatchCodexNativeHook(
17879
- {
17880
- hook_event_name: "Stop",
17881
- cwd,
17882
- session_id: "sess-stop-doc-refresh",
17883
- last_assistant_message: "Launch-ready: yes",
17884
- },
17885
- { cwd },
17886
- );
18602
+ const output = parseSingleJsonStdout(runNativeHookCli({
18603
+ hook_event_name: "Stop",
18604
+ cwd,
18605
+ session_id: "sess-stop-doc-refresh",
18606
+ last_assistant_message: "Launch-ready: yes",
18607
+ }, { cwd }));
17887
18608
 
17888
- assert.equal(result.omxEventName, "stop");
17889
- assert.equal((result.outputJson as { decision?: string } | null)?.decision, undefined);
17890
- assert.equal((result.outputJson as { hookSpecificOutput?: { hookEventName?: string } } | null)?.hookSpecificOutput?.hookEventName, "Stop");
17891
- assert.match(JSON.stringify(result.outputJson), /Document-refresh warning/);
17892
- assert.match(JSON.stringify(result.outputJson), /staged \+ unstaged changes/);
18609
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
18610
+ assert.equal("hookSpecificOutput" in output, false);
18611
+ assert.equal("decision" in output, false);
18612
+ assert.equal("reason" in output, false);
18613
+ assert.equal("stopReason" in output, false);
18614
+ assert.match(String(output.systemMessage ?? ""), /Document-refresh warning/);
18615
+ assert.match(String(output.systemMessage ?? ""), /staged \+ unstaged changes/);
17893
18616
  } finally {
17894
18617
  await rm(cwd, { recursive: true, force: true });
17895
18618
  }
@@ -20888,6 +21611,21 @@ PY`,
20888
21611
 
20889
21612
  assert.equal(result.omxEventName, "pre-tool-use");
20890
21613
  assert.equal(result.outputJson, null);
21614
+
21615
+ const allowedDraftPatch = await dispatchCodexNativeHook(
21616
+ {
21617
+ hook_event_name: "PreToolUse",
21618
+ cwd,
21619
+ session_id: nativeSessionId,
21620
+ thread_id: "thread-ralplan-native-map-draft-artifact",
21621
+ tool_name: "apply_patch",
21622
+ tool_input: {
21623
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
21624
+ },
21625
+ },
21626
+ { cwd },
21627
+ );
21628
+ assert.equal(allowedDraftPatch.outputJson, null);
20891
21629
  } finally {
20892
21630
  await rm(cwd, { recursive: true, force: true });
20893
21631
  }
@@ -21006,6 +21744,21 @@ PY`,
21006
21744
  cwd: ownerCwd,
21007
21745
  });
21008
21746
 
21747
+ const allowedForeignDraft = await dispatchCodexNativeHook(
21748
+ {
21749
+ hook_event_name: "PreToolUse",
21750
+ cwd,
21751
+ session_id: "019e-ralplan-live-root-unresolved-current",
21752
+ thread_id: "thread-ralplan-live-root-conflict",
21753
+ tool_name: "apply_patch",
21754
+ tool_input: {
21755
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
21756
+ },
21757
+ },
21758
+ { cwd },
21759
+ );
21760
+ assert.equal(allowedForeignDraft.outputJson, null);
21761
+
21009
21762
  const result = await dispatchCodexNativeHook(
21010
21763
  {
21011
21764
  hook_event_name: "PreToolUse",
@@ -21099,14 +21852,29 @@ PY`,
21099
21852
 
21100
21853
  assert.equal(result.omxEventName, "pre-tool-use");
21101
21854
  assert.equal(result.outputJson, null);
21102
- } finally {
21103
- await rm(cwd, { recursive: true, force: true });
21104
- }
21105
- });
21106
21855
 
21107
- it("does not block unrelated native Codex ids when current OMX session mapping does not match", async () => {
21108
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-unrelated-"));
21109
- try {
21856
+ const allowedDraftPatch = await dispatchCodexNativeHook(
21857
+ {
21858
+ hook_event_name: "PreToolUse",
21859
+ cwd,
21860
+ session_id: nativeSessionId,
21861
+ thread_id: "thread-ralplan-live-root-owner-draft-pass",
21862
+ tool_name: "apply_patch",
21863
+ tool_input: {
21864
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "live-root-owner.md")}\n+# Draft\n*** End Patch\n`,
21865
+ },
21866
+ },
21867
+ { cwd },
21868
+ );
21869
+ assert.equal(allowedDraftPatch.outputJson, null);
21870
+ } finally {
21871
+ await rm(cwd, { recursive: true, force: true });
21872
+ }
21873
+ });
21874
+
21875
+ it("does not block unrelated native Codex ids when current OMX session mapping does not match", async () => {
21876
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-unrelated-"));
21877
+ try {
21110
21878
  const stateDir = join(cwd, ".omx", "state");
21111
21879
  const sessionId = "sess-ralplan-native-map-owner";
21112
21880
  const ownerNativeSessionId = "019e-ralplan-native-owner";
@@ -21459,6 +22227,189 @@ PY`,
21459
22227
  }
21460
22228
  });
21461
22229
 
22230
+ it("blocks unsupported active conductor source edits with native delegation recovery guidance", async () => {
22231
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-unsupported-conductor-source-"));
22232
+ try {
22233
+ const stateDir = join(cwd, ".omx", "state");
22234
+ const sessionId = "sess-unsupported-conductor-source";
22235
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22236
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
22237
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
22238
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22239
+ active: true,
22240
+ mode: "ultragoal",
22241
+ current_phase: "planning",
22242
+ session_id: sessionId,
22243
+ });
22244
+ await writeJson(join(stateDir, "native-subagent-support.json"), {
22245
+ schema_version: 1,
22246
+ status: "unsupported",
22247
+ reason: "multi_agent_v1_unavailable",
22248
+ session_id: sessionId,
22249
+ evidence: "unknown tool: multi_agent_v1.spawn_agent",
22250
+ observed_at: new Date().toISOString(),
22251
+ cwd,
22252
+ });
22253
+
22254
+ const result = await dispatchCodexNativeHook(
22255
+ {
22256
+ hook_event_name: "PreToolUse",
22257
+ cwd,
22258
+ session_id: sessionId,
22259
+ thread_id: "thread-unsupported-conductor-source",
22260
+ tool_name: "Write",
22261
+ tool_input: { file_path: "src/runtime.ts", content: "export const value = 1;\n" },
22262
+ },
22263
+ { cwd },
22264
+ );
22265
+
22266
+ const output = result.outputJson as { decision?: string; hookSpecificOutput?: { additionalContext?: string } } | null;
22267
+ const context = String(output?.hookSpecificOutput?.additionalContext ?? "");
22268
+ assert.equal(output?.decision, "block");
22269
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: planning\)/);
22270
+ assert.match(context, /Native subagent support is unavailable in this environment/);
22271
+ assert.match(context, /Reason: multi_agent_v1_unavailable/);
22272
+ assert.match(context, /blocked\/cancelled/);
22273
+ assert.match(context, /do not call multi_agent_v1\.close_agent/i);
22274
+ } finally {
22275
+ await rm(cwd, { recursive: true, force: true });
22276
+ }
22277
+ });
22278
+
22279
+ it("keeps the Conductor unblocked: quoted redirect/source text is not a write target and terminal blocked-state writes survive genuinely unsupported native delegation (#3119)", async () => {
22280
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-quote-deadlock-"));
22281
+ try {
22282
+ const stateDir = join(cwd, ".omx", "state");
22283
+ const sessionId = "sess-conductor-3119-quote-deadlock";
22284
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22285
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
22286
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
22287
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22288
+ active: true,
22289
+ mode: "ultragoal",
22290
+ current_phase: "planning",
22291
+ session_id: sessionId,
22292
+ });
22293
+ // Native delegation is genuinely unsupported (explicit negative evidence).
22294
+ await writeJson(join(stateDir, "native-subagent-support.json"), {
22295
+ schema_version: 1,
22296
+ status: "unsupported",
22297
+ reason: "multi_agent_v1_unavailable",
22298
+ session_id: sessionId,
22299
+ evidence: "unknown tool: multi_agent_v1.spawn_agent",
22300
+ observed_at: new Date().toISOString(),
22301
+ cwd,
22302
+ });
22303
+
22304
+ const dispatch = (command: string) =>
22305
+ dispatchCodexNativeHook(
22306
+ {
22307
+ hook_event_name: "PreToolUse",
22308
+ cwd,
22309
+ session_id: sessionId,
22310
+ thread_id: "thread-conductor-3119-quote-deadlock",
22311
+ tool_name: "Bash",
22312
+ tool_input: { command },
22313
+ },
22314
+ { cwd },
22315
+ );
22316
+
22317
+ // Deadlock prevention (defect B): the Conductor must still terminalize its
22318
+ // own workflow state even when delegation is genuinely unsupported, even
22319
+ // when the JSON payload contains a `>` character.
22320
+ const terminalBlockedWrite = await dispatch(
22321
+ "omx state write --mode ultragoal --input '{\"active\":true,\"current_phase\":\"blocked\",\"reason\":\"native delegation unavailable -> terminalized\"}' --json",
22322
+ );
22323
+ assert.notEqual((terminalBlockedWrite.outputJson as { decision?: string } | null)?.decision, "block");
22324
+
22325
+ // Defect C: quoted regex/source text with redirect metacharacters is not a
22326
+ // write target, so issue creation is not falsely blocked.
22327
+ const issueCreate = await dispatch(
22328
+ "gh issue create --title x --body 'Guard misparsed regex /[^>]+>{1,2}/ as a redirect target'",
22329
+ );
22330
+ assert.notEqual((issueCreate.outputJson as { decision?: string } | null)?.decision, "block");
22331
+
22332
+ // Fail-closed preserved: a REAL unquoted redirect to a non-metadata path is
22333
+ // still blocked.
22334
+ const realRedirect = await dispatch("printf pwn > src/runtime.ts");
22335
+ assert.equal((realRedirect.outputJson as { decision?: string } | null)?.decision, "block");
22336
+ assert.match(
22337
+ String((realRedirect.outputJson as { reason?: string } | null)?.reason ?? ""),
22338
+ /not workflow state\/ledger\/mailbox\/handoff metadata/,
22339
+ );
22340
+
22341
+ // A real unquoted redirect to allowed workflow metadata still passes.
22342
+ const metadataRedirect = await dispatch("printf blocked > .omx/state/conductor.log");
22343
+ assert.notEqual((metadataRedirect.outputJson as { decision?: string } | null)?.decision, "block");
22344
+ } finally {
22345
+ await rm(cwd, { recursive: true, force: true });
22346
+ }
22347
+ });
22348
+
22349
+ it("fail-closed: escaped-quote and ANSI-C quoted redirects to source stay blocked while legit quoted data and nested shells behave (#3119)", async () => {
22350
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-escaped-quote-"));
22351
+ try {
22352
+ const stateDir = join(cwd, ".omx", "state");
22353
+ const sessionId = "sess-conductor-3119-escaped-quote";
22354
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22355
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
22356
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
22357
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22358
+ active: true,
22359
+ mode: "ultragoal",
22360
+ current_phase: "planning",
22361
+ session_id: sessionId,
22362
+ });
22363
+
22364
+ const dispatch = (command: string) =>
22365
+ dispatchCodexNativeHook(
22366
+ {
22367
+ hook_event_name: "PreToolUse",
22368
+ cwd,
22369
+ session_id: sessionId,
22370
+ thread_id: "thread-conductor-3119-escaped-quote",
22371
+ tool_name: "Bash",
22372
+ tool_input: { command },
22373
+ },
22374
+ { cwd },
22375
+ );
22376
+
22377
+ // A top-level escaped quote (\' \") is a LITERAL char in bash, not a quote
22378
+ // opener, and $'...' is ANSI-C quoting. None of these may hide the real
22379
+ // `>` redirect \u2014 all of these genuinely write src/runtime.ts and MUST block.
22380
+ const mustBlock = [
22381
+ "printf pwn > src/runtime.ts", // plain control
22382
+ "printf pwn \\' > src/runtime.ts \\'", // escaped single quote
22383
+ "printf pwn \\\" > src/runtime.ts \\\"", // escaped double quote
22384
+ "printf pwn $'\\'' > src/runtime.ts $'\\''", // ANSI-C escaped quote
22385
+ "bash -c 'printf pwn > src/runtime.ts'", // nested shell redirect
22386
+ ];
22387
+ for (const command of mustBlock) {
22388
+ const result = await dispatch(command);
22389
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
22390
+ assert.match(
22391
+ String((result.outputJson as { reason?: string } | null)?.reason ?? ""),
22392
+ /not workflow state\/ledger\/mailbox\/handoff metadata/,
22393
+ command,
22394
+ );
22395
+ }
22396
+
22397
+ // Legitimate quoted DATA metacharacters (single quotes, double quotes,
22398
+ // ANSI-C) are not redirects and must not be falsely blocked.
22399
+ const mustNotBlock = [
22400
+ "gh issue create --title x --body 'a > b regex /[^>]+>{1,2}/'",
22401
+ "echo \"value > threshold\"",
22402
+ "printf $'a>b'",
22403
+ ];
22404
+ for (const command of mustNotBlock) {
22405
+ const result = await dispatch(command);
22406
+ assert.notEqual((result.outputJson as { decision?: string } | null)?.decision, "block", command);
22407
+ }
22408
+ } finally {
22409
+ await rm(cwd, { recursive: true, force: true });
22410
+ }
22411
+ });
22412
+
21462
22413
  it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
21463
22414
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
21464
22415
  try {
@@ -21724,25 +22675,22 @@ PY`,
21724
22675
  }
21725
22676
  });
21726
22677
 
21727
- it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
21728
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
21729
- const originalOmxRoot = process.env.OMX_ROOT;
21730
- const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
21731
- const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
22678
+ it("blocks a corrupt kind:subagent leader that omits leader_thread_id via native session identity while still trusting a real non-leader child (#3117 P2)", async () => {
22679
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-corrupt-leader-no-lead-"));
22680
+ process.env.OMX_ROOT = cwd;
21732
22681
  try {
21733
- process.env.OMX_ROOT = cwd;
21734
- delete process.env.OMX_STATE_ROOT;
21735
- delete process.env.OMX_TEAM_STATE_ROOT;
21736
22682
  const stateDir = join(cwd, ".omx", "state");
21737
- const sessionId = "sess-conductor-thread-spawn-leader";
21738
- const leaderThreadId = "thread-conductor-thread-spawn-leader";
21739
- const nowIso = new Date().toISOString();
22683
+ const sessionId = "sess-3117-corrupt-leader-no-lead";
22684
+ const leaderThreadId = "thread-3117-corrupt-leader-no-lead-leader";
22685
+ const childThreadId = "thread-3117-corrupt-leader-no-lead-child";
21740
22686
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22687
+ // native_session_id anchors the leader identity even though the tracker omits
22688
+ // leader_thread_id and corruptly labels the leader kind:"subagent".
21741
22689
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
21742
- await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "executing");
21743
- await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22690
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
22691
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
21744
22692
  active: true,
21745
- mode: "ultragoal",
22693
+ mode: "ralph",
21746
22694
  current_phase: "executing",
21747
22695
  session_id: sessionId,
21748
22696
  });
@@ -21751,54 +22699,59 @@ PY`,
21751
22699
  sessions: {
21752
22700
  [sessionId]: {
21753
22701
  session_id: sessionId,
21754
- leader_thread_id: leaderThreadId,
21755
- updated_at: nowIso,
21756
22702
  threads: {
21757
- [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
22703
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22704
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
21758
22705
  },
21759
22706
  },
21760
22707
  },
21761
22708
  });
21762
22709
 
21763
- const result = await dispatchCodexNativeHook(
22710
+ // Untyped leader payload must stay blocked: leader identity is anchored to
22711
+ // native_session_id, not the corrupt tracker kind.
22712
+ const untypedLeader = await dispatchCodexNativeHook(
21764
22713
  {
21765
22714
  hook_event_name: "PreToolUse",
21766
22715
  cwd,
21767
22716
  session_id: sessionId,
21768
22717
  thread_id: leaderThreadId,
21769
- agent_role: "executor",
21770
- source: {
21771
- subagent: {
21772
- thread_spawn: {
21773
- parent_thread_id: leaderThreadId,
21774
- },
21775
- },
21776
- },
21777
22718
  tool_name: "apply_patch",
21778
- tool_input: { file_path: "configs/ws/kalshi_ws_auth_evidence.json" },
22719
+ tool_input: { file_path: "src/feature.ts" },
21779
22720
  },
21780
22721
  { cwd },
21781
22722
  );
22723
+ assert.equal(untypedLeader.outputJson?.decision, "block");
22724
+ assert.match(String(untypedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
21782
22725
 
21783
- assert.equal(result.outputJson?.decision, "block");
21784
- assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: executing\)/);
22726
+ // Control: a genuine non-leader child stays trusted (P1 preserved).
22727
+ const untypedChild = await dispatchCodexNativeHook(
22728
+ {
22729
+ hook_event_name: "PreToolUse",
22730
+ cwd,
22731
+ session_id: sessionId,
22732
+ thread_id: childThreadId,
22733
+ tool_name: "apply_patch",
22734
+ tool_input: { file_path: "src/feature.ts" },
22735
+ },
22736
+ { cwd },
22737
+ );
22738
+ assert.equal(untypedChild.outputJson, null);
21785
22739
  } finally {
21786
- if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
21787
- else process.env.OMX_ROOT = originalOmxRoot;
21788
- if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
21789
- else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
21790
- if (originalOmxTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
21791
- else process.env.OMX_TEAM_STATE_ROOT = originalOmxTeamStateRoot;
21792
22740
  await rm(cwd, { recursive: true, force: true });
21793
22741
  }
21794
22742
  });
21795
22743
 
21796
- it("blocks Main-root ralph conductor source and planning artifact writes while allowing .omx workflow state writes", async () => {
21797
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-conductor-write-"));
22744
+ it("fails closed for untyped provenance when no authoritative leader anchor exists (#3117 P2)", async () => {
22745
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-no-leader-anchor-"));
22746
+ process.env.OMX_ROOT = cwd;
21798
22747
  try {
21799
22748
  const stateDir = join(cwd, ".omx", "state");
21800
- const sessionId = "sess-ralph-conductor-write";
22749
+ const sessionId = "sess-3117-no-leader-anchor";
22750
+ const leaderThreadId = "thread-3117-no-leader-anchor-leader";
22751
+ const childThreadId = "thread-3117-no-leader-anchor-child";
21801
22752
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22753
+ // No native_session_id / owner ids and no tracker leader_thread_id: the leader is
22754
+ // unidentifiable, so untyped provenance must not be inferred from kind:"subagent".
21802
22755
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
21803
22756
  await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
21804
22757
  await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
@@ -21807,71 +22760,754 @@ PY`,
21807
22760
  current_phase: "executing",
21808
22761
  session_id: sessionId,
21809
22762
  });
21810
-
21811
- const blocked = await dispatchCodexNativeHook(
21812
- {
21813
- hook_event_name: "PreToolUse",
21814
- cwd,
21815
- session_id: sessionId,
21816
- thread_id: "thread-ralph-conductor-write",
21817
- tool_name: "Edit",
21818
- tool_input: { file_path: "src/runtime.ts" },
22763
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22764
+ schemaVersion: 1,
22765
+ sessions: {
22766
+ [sessionId]: {
22767
+ session_id: sessionId,
22768
+ threads: {
22769
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22770
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
22771
+ },
22772
+ },
21819
22773
  },
21820
- { cwd },
21821
- );
21822
- assert.equal(blocked.outputJson?.decision, "block");
21823
- assert.match(String(blocked.outputJson?.reason ?? ""), /ralph phase: executing/);
22774
+ });
21824
22775
 
21825
- for (const [toolName, filePath] of [
21826
- ["Write", ".omx/plans/conductor-owned-plan.md"],
21827
- ["Edit", ".omx/specs/conductor-owned-spec.md"],
21828
- ] as const) {
21829
- const planningArtifactWrite = await dispatchCodexNativeHook(
22776
+ for (const threadId of [leaderThreadId, childThreadId]) {
22777
+ const result = await dispatchCodexNativeHook(
21830
22778
  {
21831
22779
  hook_event_name: "PreToolUse",
21832
22780
  cwd,
21833
22781
  session_id: sessionId,
21834
- thread_id: "thread-ralph-conductor-write",
21835
- tool_name: toolName,
21836
- tool_input: { file_path: filePath },
22782
+ thread_id: threadId,
22783
+ tool_name: "apply_patch",
22784
+ tool_input: { file_path: "src/feature.ts" },
21837
22785
  },
21838
22786
  { cwd },
21839
22787
  );
21840
- assert.equal(planningArtifactWrite.outputJson?.decision, "block", `${toolName} ${filePath}`);
21841
- assert.match(String(planningArtifactWrite.outputJson?.reason ?? ""), /plan\/code writes are blocked/);
22788
+ assert.equal(result.outputJson?.decision, "block", threadId);
22789
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
21842
22790
  }
22791
+ } finally {
22792
+ await rm(cwd, { recursive: true, force: true });
22793
+ }
22794
+ });
21843
22795
 
21844
- const allowed = await dispatchCodexNativeHook(
21845
- {
21846
- hook_event_name: "PreToolUse",
21847
- cwd,
21848
- session_id: sessionId,
21849
- thread_id: "thread-ralph-conductor-write",
21850
- tool_name: "Write",
21851
- tool_input: { file_path: ".omx/state/sessions/sess-ralph-conductor-write/ralph-state.json" },
22796
+ it("does not borrow leader anchors from a foreign root session.json when evaluating another session (#3117 P3)", async () => {
22797
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-cross-session-anchor-"));
22798
+ process.env.OMX_ROOT = cwd;
22799
+ try {
22800
+ const stateDir = join(cwd, ".omx", "state");
22801
+ const foreignSessionId = "sess-3117-p3-foreign-A";
22802
+ const foreignLeaderThreadId = "thread-3117-p3-A-leader";
22803
+ const sessionId = "sess-3117-p3-checked-B";
22804
+ const leaderThreadId = "thread-3117-p3-B-leader";
22805
+ const childThreadId = "thread-3117-p3-B-child";
22806
+ const sessionPath = join(stateDir, "session.json");
22807
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22808
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
22809
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
22810
+ active: true,
22811
+ mode: "ralph",
22812
+ current_phase: "executing",
22813
+ session_id: sessionId,
22814
+ });
22815
+ // Evaluated session B has a corrupt tracker: no leader_thread_id, leader mislabeled kind:"subagent".
22816
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22817
+ schemaVersion: 1,
22818
+ sessions: {
22819
+ [sessionId]: {
22820
+ session_id: sessionId,
22821
+ threads: {
22822
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22823
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
22824
+ },
22825
+ },
21852
22826
  },
21853
- { cwd },
21854
- );
21855
- assert.equal(allowed.outputJson, null);
22827
+ });
21856
22828
 
21857
- const protectedRawState = await dispatchCodexNativeHook(
22829
+ const dispatchThread = async (threadId: string) => dispatchCodexNativeHook(
21858
22830
  {
21859
22831
  hook_event_name: "PreToolUse",
21860
22832
  cwd,
21861
22833
  session_id: sessionId,
21862
- thread_id: "thread-ralph-conductor-write",
21863
- tool_name: "Write",
21864
- tool_input: { file_path: ".omx/state/sessions/sess-ralph-conductor-write/autopilot-state.json" },
22834
+ thread_id: threadId,
22835
+ tool_name: "apply_patch",
22836
+ tool_input: { file_path: "src/feature.ts" },
21865
22837
  },
21866
22838
  { cwd },
21867
22839
  );
21868
- assert.equal(protectedRawState.outputJson?.decision, "block");
21869
22840
 
21870
- const safeTransport = await dispatchCodexNativeHook(
21871
- {
21872
- hook_event_name: "PreToolUse",
21873
- cwd,
21874
- session_id: sessionId,
22841
+ // Root session.json owns a DIFFERENT session A: its native/owner ids must not
22842
+ // anchor session B, so B's mislabeled leader stays blocked (fail closed).
22843
+ await writeJson(sessionPath, { session_id: foreignSessionId, native_session_id: foreignLeaderThreadId });
22844
+ const foreignLeader = await dispatchThread(leaderThreadId);
22845
+ assert.equal(foreignLeader.outputJson?.decision, "block");
22846
+ assert.match(String(foreignLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22847
+ const foreignChild = await dispatchThread(childThreadId);
22848
+ assert.equal(foreignChild.outputJson?.decision, "block");
22849
+
22850
+ // Control: when the root session.json owns the evaluated session B, its
22851
+ // native_session_id anchors the leader (blocked) while a genuine child is trusted.
22852
+ await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
22853
+ const ownedLeader = await dispatchThread(leaderThreadId);
22854
+ assert.equal(ownedLeader.outputJson?.decision, "block");
22855
+ assert.match(String(ownedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22856
+ const ownedChild = await dispatchThread(childThreadId);
22857
+ assert.equal(ownedChild.outputJson, null);
22858
+ } finally {
22859
+ await rm(cwd, { recursive: true, force: true });
22860
+ }
22861
+ });
22862
+
22863
+ it("fails closed when session.json carries only owner session ids without a leader thread anchor (#3117 P4)", async () => {
22864
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-owner-only-anchor-"));
22865
+ process.env.OMX_ROOT = cwd;
22866
+ try {
22867
+ const stateDir = join(cwd, ".omx", "state");
22868
+ const sessionId = "sess-3117-p4-owner-only";
22869
+ const leaderThreadId = "thread-3117-p4-leader";
22870
+ const childThreadId = "thread-3117-p4-child";
22871
+ const untrackedThreadId = "thread-3117-p4-untracked";
22872
+ const sessionPath = join(stateDir, "session.json");
22873
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22874
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
22875
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
22876
+ active: true,
22877
+ mode: "ralph",
22878
+ current_phase: "executing",
22879
+ session_id: sessionId,
22880
+ });
22881
+ // Corrupt tracker for the evaluated session: no leader_thread_id, leader mislabeled kind:"subagent".
22882
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22883
+ schemaVersion: 1,
22884
+ sessions: {
22885
+ [sessionId]: {
22886
+ session_id: sessionId,
22887
+ threads: {
22888
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22889
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
22890
+ },
22891
+ },
22892
+ },
22893
+ });
22894
+
22895
+ const dispatchThread = async (threadId: string) => dispatchCodexNativeHook(
22896
+ {
22897
+ hook_event_name: "PreToolUse",
22898
+ cwd,
22899
+ session_id: sessionId,
22900
+ thread_id: threadId,
22901
+ tool_name: "apply_patch",
22902
+ tool_input: { file_path: "src/feature.ts" },
22903
+ },
22904
+ { cwd },
22905
+ );
22906
+
22907
+ // session.json maps to the evaluated session via owner ids but has NO
22908
+ // native_session_id: owner session ids are not leader thread anchors, so trust
22909
+ // must fail closed rather than treat their presence as an anchor (#3117 P4).
22910
+ await writeJson(sessionPath, {
22911
+ session_id: sessionId,
22912
+ owner_omx_session_id: "owner-omx-3117-p4",
22913
+ owner_codex_session_id: "owner-codex-3117-p4",
22914
+ });
22915
+ const ownerOnlyLeader = await dispatchThread(leaderThreadId);
22916
+ assert.equal(ownerOnlyLeader.outputJson?.decision, "block");
22917
+ assert.match(String(ownerOnlyLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22918
+ // Control: an untracked thread also stays blocked under the same state.
22919
+ const ownerOnlyUntracked = await dispatchThread(untrackedThreadId);
22920
+ assert.equal(ownerOnlyUntracked.outputJson?.decision, "block");
22921
+
22922
+ // Control: once a genuine native_session_id leader thread anchor exists for this
22923
+ // session, the leader is blocked via that anchor while a real child is trusted.
22924
+ await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
22925
+ const anchoredLeader = await dispatchThread(leaderThreadId);
22926
+ assert.equal(anchoredLeader.outputJson?.decision, "block");
22927
+ assert.match(String(anchoredLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22928
+ const anchoredChild = await dispatchThread(childThreadId);
22929
+ assert.equal(anchoredChild.outputJson, null);
22930
+ } finally {
22931
+ await rm(cwd, { recursive: true, force: true });
22932
+ }
22933
+ });
22934
+
22935
+ it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
22936
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
22937
+ const originalOmxRoot = process.env.OMX_ROOT;
22938
+ const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
22939
+ const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
22940
+ try {
22941
+ process.env.OMX_ROOT = cwd;
22942
+ delete process.env.OMX_STATE_ROOT;
22943
+ delete process.env.OMX_TEAM_STATE_ROOT;
22944
+ const stateDir = join(cwd, ".omx", "state");
22945
+ const sessionId = "sess-conductor-thread-spawn-leader";
22946
+ const leaderThreadId = "thread-conductor-thread-spawn-leader";
22947
+ const nowIso = new Date().toISOString();
22948
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22949
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
22950
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "executing");
22951
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22952
+ active: true,
22953
+ mode: "ultragoal",
22954
+ current_phase: "executing",
22955
+ session_id: sessionId,
22956
+ });
22957
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22958
+ schemaVersion: 1,
22959
+ sessions: {
22960
+ [sessionId]: {
22961
+ session_id: sessionId,
22962
+ leader_thread_id: leaderThreadId,
22963
+ updated_at: nowIso,
22964
+ threads: {
22965
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
22966
+ },
22967
+ },
22968
+ },
22969
+ });
22970
+
22971
+ const result = await dispatchCodexNativeHook(
22972
+ {
22973
+ hook_event_name: "PreToolUse",
22974
+ cwd,
22975
+ session_id: sessionId,
22976
+ thread_id: leaderThreadId,
22977
+ agent_role: "executor",
22978
+ source: {
22979
+ subagent: {
22980
+ thread_spawn: {
22981
+ parent_thread_id: leaderThreadId,
22982
+ },
22983
+ },
22984
+ },
22985
+ tool_name: "apply_patch",
22986
+ tool_input: { file_path: "configs/ws/kalshi_ws_auth_evidence.json" },
22987
+ },
22988
+ { cwd },
22989
+ );
22990
+
22991
+ assert.equal(result.outputJson?.decision, "block");
22992
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: executing\)/);
22993
+ } finally {
22994
+ if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
22995
+ else process.env.OMX_ROOT = originalOmxRoot;
22996
+ if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
22997
+ else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
22998
+ if (originalOmxTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
22999
+ else process.env.OMX_TEAM_STATE_ROOT = originalOmxTeamStateRoot;
23000
+ await rm(cwd, { recursive: true, force: true });
23001
+ }
23002
+ });
23003
+
23004
+ it("denies Main-root conductor apply_patch from the session leader thread during active Ralph (#3116)", async () => {
23005
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-root-denial-"));
23006
+ process.env.OMX_ROOT = cwd;
23007
+ try {
23008
+ const stateDir = join(cwd, ".omx", "state");
23009
+ const sessionId = "sess-3116-root-denial";
23010
+ const leaderThreadId = "thread-3116-root-denial-leader";
23011
+ const nowIso = new Date().toISOString();
23012
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23013
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23014
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23015
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23016
+ active: true,
23017
+ mode: "ralph",
23018
+ current_phase: "executing",
23019
+ session_id: sessionId,
23020
+ });
23021
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23022
+ schemaVersion: 1,
23023
+ sessions: {
23024
+ [sessionId]: {
23025
+ session_id: sessionId,
23026
+ leader_thread_id: leaderThreadId,
23027
+ updated_at: nowIso,
23028
+ threads: {
23029
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23030
+ },
23031
+ },
23032
+ },
23033
+ });
23034
+
23035
+ const result = await dispatchCodexNativeHook(
23036
+ {
23037
+ hook_event_name: "PreToolUse",
23038
+ cwd,
23039
+ session_id: sessionId,
23040
+ thread_id: leaderThreadId,
23041
+ tool_name: "apply_patch",
23042
+ tool_input: { file_path: "src/feature.ts" },
23043
+ },
23044
+ { cwd },
23045
+ );
23046
+
23047
+ assert.equal(result.outputJson?.decision, "block");
23048
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
23049
+ } finally {
23050
+ await rm(cwd, { recursive: true, force: true });
23051
+ }
23052
+ });
23053
+
23054
+ it("allows apply_patch from an untyped collaboration.spawn_agent child tracked as a subagent during active Ralph (#3116)", async () => {
23055
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-trusted-child-"));
23056
+ process.env.OMX_ROOT = cwd;
23057
+ try {
23058
+ const stateDir = join(cwd, ".omx", "state");
23059
+ const sessionId = "sess-3116-trusted-child";
23060
+ const leaderThreadId = "thread-3116-trusted-child-leader";
23061
+ const childThreadId = "thread-3116-trusted-child-worker";
23062
+ const nowIso = new Date().toISOString();
23063
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23064
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23065
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23066
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23067
+ active: true,
23068
+ mode: "ralph",
23069
+ current_phase: "executing",
23070
+ session_id: sessionId,
23071
+ });
23072
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23073
+ schemaVersion: 1,
23074
+ sessions: {
23075
+ [sessionId]: {
23076
+ session_id: sessionId,
23077
+ leader_thread_id: leaderThreadId,
23078
+ updated_at: nowIso,
23079
+ threads: {
23080
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23081
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
23082
+ },
23083
+ },
23084
+ },
23085
+ });
23086
+
23087
+ // Untyped role (not in the typed-agent catalog) must not defeat tracker-backed trust.
23088
+ const result = await dispatchCodexNativeHook(
23089
+ {
23090
+ hook_event_name: "PreToolUse",
23091
+ cwd,
23092
+ session_id: sessionId,
23093
+ thread_id: childThreadId,
23094
+ agent_role: "collaboration-child",
23095
+ source: {
23096
+ subagent: {
23097
+ thread_spawn: {
23098
+ parent_thread_id: leaderThreadId,
23099
+ depth: 1,
23100
+ agent_nickname: "Helper",
23101
+ },
23102
+ },
23103
+ },
23104
+ tool_name: "apply_patch",
23105
+ tool_input: { file_path: "src/feature.ts" },
23106
+ },
23107
+ { cwd },
23108
+ );
23109
+
23110
+ assert.equal(result.outputJson, null);
23111
+ } finally {
23112
+ await rm(cwd, { recursive: true, force: true });
23113
+ }
23114
+ });
23115
+
23116
+ it("allows apply_patch from untyped collaboration descendants via tracked thread and tracked parent chain (#3116)", async () => {
23117
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-descendant-"));
23118
+ process.env.OMX_ROOT = cwd;
23119
+ try {
23120
+ const stateDir = join(cwd, ".omx", "state");
23121
+ const sessionId = "sess-3116-descendant";
23122
+ const leaderThreadId = "thread-3116-descendant-leader";
23123
+ const childThreadId = "thread-3116-descendant-child";
23124
+ const grandchildThreadId = "thread-3116-descendant-grandchild";
23125
+ const greatGrandchildThreadId = "thread-3116-descendant-great-grandchild";
23126
+ const nowIso = new Date().toISOString();
23127
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23128
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23129
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23130
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23131
+ active: true,
23132
+ mode: "ralph",
23133
+ current_phase: "executing",
23134
+ session_id: sessionId,
23135
+ });
23136
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23137
+ schemaVersion: 1,
23138
+ sessions: {
23139
+ [sessionId]: {
23140
+ session_id: sessionId,
23141
+ leader_thread_id: leaderThreadId,
23142
+ updated_at: nowIso,
23143
+ threads: {
23144
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23145
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
23146
+ [grandchildThreadId]: { thread_id: grandchildThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: childThreadId },
23147
+ },
23148
+ },
23149
+ },
23150
+ });
23151
+
23152
+ // (a) A descendant whose own thread is tracked as a subagent is trusted directly.
23153
+ const trackedDescendant = await dispatchCodexNativeHook(
23154
+ {
23155
+ hook_event_name: "PreToolUse",
23156
+ cwd,
23157
+ session_id: sessionId,
23158
+ thread_id: grandchildThreadId,
23159
+ tool_name: "apply_patch",
23160
+ tool_input: { file_path: "src/feature.ts" },
23161
+ },
23162
+ { cwd },
23163
+ );
23164
+ assert.equal(trackedDescendant.outputJson, null);
23165
+
23166
+ // (b) A not-yet-tracked descendant is trusted when its runtime spawn parent is a tracked thread.
23167
+ const chainedDescendant = await dispatchCodexNativeHook(
23168
+ {
23169
+ hook_event_name: "PreToolUse",
23170
+ cwd,
23171
+ session_id: sessionId,
23172
+ thread_id: greatGrandchildThreadId,
23173
+ agent_role: "collaboration-child",
23174
+ source: {
23175
+ subagent: {
23176
+ thread_spawn: {
23177
+ parent_thread_id: grandchildThreadId,
23178
+ depth: 3,
23179
+ },
23180
+ },
23181
+ },
23182
+ tool_name: "apply_patch",
23183
+ tool_input: { file_path: "src/feature.ts" },
23184
+ },
23185
+ { cwd },
23186
+ );
23187
+ assert.equal(chainedDescendant.outputJson, null);
23188
+ } finally {
23189
+ await rm(cwd, { recursive: true, force: true });
23190
+ }
23191
+ });
23192
+
23193
+ it("resolves the session-pinned Ralph state so collaboration children edit while the leader stays blocked (#3116)", async () => {
23194
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-pinned-state-"));
23195
+ process.env.OMX_ROOT = cwd;
23196
+ try {
23197
+ const stateDir = join(cwd, ".omx", "state");
23198
+ const sessionId = "sess-3116-pinned-state";
23199
+ const leaderThreadId = "thread-3116-pinned-state-leader";
23200
+ const childThreadId = "thread-3116-pinned-state-child";
23201
+ const nowIso = new Date().toISOString();
23202
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23203
+ // Native session id differs from the canonical session id: the guard must resolve it.
23204
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23205
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23206
+ const ralphStatePath = join(stateDir, "sessions", sessionId, "ralph-state.json");
23207
+ await writeJson(ralphStatePath, {
23208
+ active: true,
23209
+ mode: "ralph",
23210
+ current_phase: "executing",
23211
+ session_id: sessionId,
23212
+ });
23213
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23214
+ schemaVersion: 1,
23215
+ sessions: {
23216
+ [sessionId]: {
23217
+ session_id: sessionId,
23218
+ leader_thread_id: leaderThreadId,
23219
+ updated_at: nowIso,
23220
+ threads: {
23221
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23222
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
23223
+ },
23224
+ },
23225
+ },
23226
+ });
23227
+
23228
+ // Payload carries the native session id, which must map to the canonical session.
23229
+ const childEdit = await dispatchCodexNativeHook(
23230
+ {
23231
+ hook_event_name: "PreToolUse",
23232
+ cwd,
23233
+ session_id: leaderThreadId,
23234
+ thread_id: childThreadId,
23235
+ tool_name: "apply_patch",
23236
+ tool_input: { file_path: "src/feature.ts" },
23237
+ },
23238
+ { cwd },
23239
+ );
23240
+ assert.equal(childEdit.outputJson, null);
23241
+
23242
+ const leaderEdit = await dispatchCodexNativeHook(
23243
+ {
23244
+ hook_event_name: "PreToolUse",
23245
+ cwd,
23246
+ session_id: leaderThreadId,
23247
+ thread_id: leaderThreadId,
23248
+ tool_name: "apply_patch",
23249
+ tool_input: { file_path: "src/feature.ts" },
23250
+ },
23251
+ { cwd },
23252
+ );
23253
+ assert.equal(leaderEdit.outputJson?.decision, "block");
23254
+ assert.match(String(leaderEdit.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
23255
+
23256
+ // The guard reads the session-pinned phase: flipping it to "starting" releases the leader too.
23257
+ await writeJson(ralphStatePath, {
23258
+ active: true,
23259
+ mode: "ralph",
23260
+ current_phase: "starting",
23261
+ session_id: sessionId,
23262
+ });
23263
+ const leaderEditAfterStarting = await dispatchCodexNativeHook(
23264
+ {
23265
+ hook_event_name: "PreToolUse",
23266
+ cwd,
23267
+ session_id: leaderThreadId,
23268
+ thread_id: leaderThreadId,
23269
+ tool_name: "apply_patch",
23270
+ tool_input: { file_path: "src/feature.ts" },
23271
+ },
23272
+ { cwd },
23273
+ );
23274
+ assert.equal(leaderEditAfterStarting.outputJson, null);
23275
+ } finally {
23276
+ await rm(cwd, { recursive: true, force: true });
23277
+ }
23278
+ });
23279
+
23280
+ it("trusts the native collaboration.spawn_agent child surface via runtime spawn provenance during active Ralph (#3116)", async () => {
23281
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-collab-surface-"));
23282
+ process.env.OMX_ROOT = cwd;
23283
+ try {
23284
+ const stateDir = join(cwd, ".omx", "state");
23285
+ const sessionId = "sess-3116-collab-surface";
23286
+ const leaderThreadId = "thread-3116-collab-surface-leader";
23287
+ const childThreadId = "thread-3116-collab-surface-child";
23288
+ const nowIso = new Date().toISOString();
23289
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23290
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23291
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23292
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23293
+ active: true,
23294
+ mode: "ralph",
23295
+ current_phase: "executing",
23296
+ session_id: sessionId,
23297
+ });
23298
+ // Child SessionStart has not been recorded yet: only the leader is tracked.
23299
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23300
+ schemaVersion: 1,
23301
+ sessions: {
23302
+ [sessionId]: {
23303
+ session_id: sessionId,
23304
+ leader_thread_id: leaderThreadId,
23305
+ updated_at: nowIso,
23306
+ threads: {
23307
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23308
+ },
23309
+ },
23310
+ },
23311
+ });
23312
+
23313
+ // No typed agent_role at all: a raw collaboration.spawn_agent child payload shape.
23314
+ const result = await dispatchCodexNativeHook(
23315
+ {
23316
+ hook_event_name: "PreToolUse",
23317
+ cwd,
23318
+ session_id: sessionId,
23319
+ thread_id: childThreadId,
23320
+ source: {
23321
+ subagent: {
23322
+ thread_spawn: {
23323
+ parent_thread_id: leaderThreadId,
23324
+ depth: 1,
23325
+ agent_nickname: "Implementer",
23326
+ },
23327
+ },
23328
+ },
23329
+ tool_name: "apply_patch",
23330
+ tool_input: { file_path: "src/feature.ts" },
23331
+ },
23332
+ { cwd },
23333
+ );
23334
+
23335
+ assert.equal(result.outputJson, null);
23336
+ } finally {
23337
+ await rm(cwd, { recursive: true, force: true });
23338
+ }
23339
+ });
23340
+
23341
+ it("denies spoofed or untrusted collaboration provenance while the main root stays protected during active Ralph (#3116)", async () => {
23342
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-spoof-denial-"));
23343
+ process.env.OMX_ROOT = cwd;
23344
+ try {
23345
+ const stateDir = join(cwd, ".omx", "state");
23346
+ const sessionId = "sess-3116-spoof-denial";
23347
+ const leaderThreadId = "thread-3116-spoof-denial-leader";
23348
+ const nowIso = new Date().toISOString();
23349
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23350
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23351
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23352
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23353
+ active: true,
23354
+ mode: "ralph",
23355
+ current_phase: "executing",
23356
+ session_id: sessionId,
23357
+ });
23358
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23359
+ schemaVersion: 1,
23360
+ sessions: {
23361
+ [sessionId]: {
23362
+ session_id: sessionId,
23363
+ leader_thread_id: leaderThreadId,
23364
+ updated_at: nowIso,
23365
+ threads: {
23366
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23367
+ },
23368
+ },
23369
+ },
23370
+ });
23371
+
23372
+ // (a) Untracked thread whose declared parent is neither the leader nor a tracked thread.
23373
+ const orphanParent = await dispatchCodexNativeHook(
23374
+ {
23375
+ hook_event_name: "PreToolUse",
23376
+ cwd,
23377
+ session_id: sessionId,
23378
+ thread_id: "thread-3116-spoof-orphan",
23379
+ agent_role: "collaboration-child",
23380
+ source: {
23381
+ subagent: {
23382
+ thread_spawn: { parent_thread_id: "thread-3116-spoof-outsider" },
23383
+ },
23384
+ },
23385
+ tool_name: "apply_patch",
23386
+ tool_input: { file_path: "src/feature.ts" },
23387
+ },
23388
+ { cwd },
23389
+ );
23390
+ assert.equal(orphanParent.outputJson?.decision, "block");
23391
+ assert.match(String(orphanParent.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
23392
+
23393
+ // (b) Untyped child with no provenance at all (no tracker thread, no spawn source).
23394
+ const noProvenance = await dispatchCodexNativeHook(
23395
+ {
23396
+ hook_event_name: "PreToolUse",
23397
+ cwd,
23398
+ session_id: sessionId,
23399
+ thread_id: "thread-3116-spoof-bare",
23400
+ agent_role: "collaboration-child",
23401
+ tool_name: "apply_patch",
23402
+ tool_input: { file_path: "src/feature.ts" },
23403
+ },
23404
+ { cwd },
23405
+ );
23406
+ assert.equal(noProvenance.outputJson?.decision, "block");
23407
+
23408
+ // (c) Spawn provenance attached to the leader thread cannot promote the main root.
23409
+ const leaderSelfSpawn = await dispatchCodexNativeHook(
23410
+ {
23411
+ hook_event_name: "PreToolUse",
23412
+ cwd,
23413
+ session_id: sessionId,
23414
+ thread_id: leaderThreadId,
23415
+ agent_role: "collaboration-child",
23416
+ source: {
23417
+ subagent: {
23418
+ thread_spawn: { parent_thread_id: leaderThreadId },
23419
+ },
23420
+ },
23421
+ tool_name: "apply_patch",
23422
+ tool_input: { file_path: "src/feature.ts" },
23423
+ },
23424
+ { cwd },
23425
+ );
23426
+ assert.equal(leaderSelfSpawn.outputJson?.decision, "block");
23427
+ } finally {
23428
+ await rm(cwd, { recursive: true, force: true });
23429
+ }
23430
+ });
23431
+
23432
+ it("blocks Main-root ralph conductor source and planning artifact writes while allowing .omx workflow state writes", async () => {
23433
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-conductor-write-"));
23434
+ try {
23435
+ const stateDir = join(cwd, ".omx", "state");
23436
+ const sessionId = "sess-ralph-conductor-write";
23437
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23438
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
23439
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23440
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23441
+ active: true,
23442
+ mode: "ralph",
23443
+ current_phase: "executing",
23444
+ session_id: sessionId,
23445
+ });
23446
+
23447
+ const blocked = await dispatchCodexNativeHook(
23448
+ {
23449
+ hook_event_name: "PreToolUse",
23450
+ cwd,
23451
+ session_id: sessionId,
23452
+ thread_id: "thread-ralph-conductor-write",
23453
+ tool_name: "Edit",
23454
+ tool_input: { file_path: "src/runtime.ts" },
23455
+ },
23456
+ { cwd },
23457
+ );
23458
+ assert.equal(blocked.outputJson?.decision, "block");
23459
+ assert.match(String(blocked.outputJson?.reason ?? ""), /ralph phase: executing/);
23460
+
23461
+ for (const [toolName, filePath] of [
23462
+ ["Write", ".omx/plans/conductor-owned-plan.md"],
23463
+ ["Edit", ".omx/specs/conductor-owned-spec.md"],
23464
+ ] as const) {
23465
+ const planningArtifactWrite = await dispatchCodexNativeHook(
23466
+ {
23467
+ hook_event_name: "PreToolUse",
23468
+ cwd,
23469
+ session_id: sessionId,
23470
+ thread_id: "thread-ralph-conductor-write",
23471
+ tool_name: toolName,
23472
+ tool_input: { file_path: filePath },
23473
+ },
23474
+ { cwd },
23475
+ );
23476
+ assert.equal(planningArtifactWrite.outputJson?.decision, "block", `${toolName} ${filePath}`);
23477
+ assert.match(String(planningArtifactWrite.outputJson?.reason ?? ""), /plan\/code writes are blocked/);
23478
+ }
23479
+
23480
+ const allowed = await dispatchCodexNativeHook(
23481
+ {
23482
+ hook_event_name: "PreToolUse",
23483
+ cwd,
23484
+ session_id: sessionId,
23485
+ thread_id: "thread-ralph-conductor-write",
23486
+ tool_name: "Write",
23487
+ tool_input: { file_path: ".omx/state/sessions/sess-ralph-conductor-write/ralph-state.json" },
23488
+ },
23489
+ { cwd },
23490
+ );
23491
+ assert.equal(allowed.outputJson, null);
23492
+
23493
+ const protectedRawState = await dispatchCodexNativeHook(
23494
+ {
23495
+ hook_event_name: "PreToolUse",
23496
+ cwd,
23497
+ session_id: sessionId,
23498
+ thread_id: "thread-ralph-conductor-write",
23499
+ tool_name: "Write",
23500
+ tool_input: { file_path: ".omx/state/sessions/sess-ralph-conductor-write/autopilot-state.json" },
23501
+ },
23502
+ { cwd },
23503
+ );
23504
+ assert.equal(protectedRawState.outputJson?.decision, "block");
23505
+
23506
+ const safeTransport = await dispatchCodexNativeHook(
23507
+ {
23508
+ hook_event_name: "PreToolUse",
23509
+ cwd,
23510
+ session_id: sessionId,
21875
23511
  thread_id: "thread-ralph-conductor-write",
21876
23512
  tool_name: "Bash",
21877
23513
  tool_input: { command: "omx state write --input '{\"mode\":\"ralph\",\"current_phase\":\"executing\",\"active\":true}' --json" },
@@ -21884,6 +23520,75 @@ PY`,
21884
23520
  }
21885
23521
  });
21886
23522
 
23523
+ it("allows structured ultragoal steering cleanup through dynamic nested shell loops", async () => {
23524
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-cleanup-"));
23525
+ try {
23526
+ const stateDir = join(cwd, ".omx", "state");
23527
+ const sessionId = "sess-ultragoal-steer-cleanup";
23528
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23529
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
23530
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
23531
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
23532
+ active: true,
23533
+ mode: "ultragoal",
23534
+ current_phase: "planning",
23535
+ session_id: sessionId,
23536
+ });
23537
+
23538
+ const result = await dispatchCodexNativeHook(
23539
+ {
23540
+ hook_event_name: "PreToolUse",
23541
+ cwd,
23542
+ session_id: sessionId,
23543
+ thread_id: "thread-ultragoal-steer-cleanup",
23544
+ tool_name: "Bash",
23545
+ tool_input: {
23546
+ command: `bash -lc 'for goal_id in G001-atomized G002-atomized; do omx ultragoal steer --kind mark_blocked_superseded --target-goal-id "$goal_id" --evidence ".omx/ultragoal cleanup supersedes atomized pseudo-goals." --rationale "Structured steering cleanup keeps durable Ultragoal metadata auditable." --json; done'`,
23547
+ },
23548
+ },
23549
+ { cwd },
23550
+ );
23551
+
23552
+ assert.equal(result.outputJson, null);
23553
+ } finally {
23554
+ await rm(cwd, { recursive: true, force: true });
23555
+ }
23556
+ });
23557
+
23558
+ it("blocks unsafe dynamic nested shell writes in conductor mode", async () => {
23559
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-dynamic-shell-write-block-"));
23560
+ try {
23561
+ const stateDir = join(cwd, ".omx", "state");
23562
+ const sessionId = "sess-dynamic-shell-write-block";
23563
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23564
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
23565
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
23566
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
23567
+ active: true,
23568
+ mode: "ultragoal",
23569
+ current_phase: "planning",
23570
+ session_id: sessionId,
23571
+ });
23572
+
23573
+ const result = await dispatchCodexNativeHook(
23574
+ {
23575
+ hook_event_name: "PreToolUse",
23576
+ cwd,
23577
+ session_id: sessionId,
23578
+ thread_id: "thread-dynamic-shell-write-block",
23579
+ tool_name: "Bash",
23580
+ tool_input: { command: `bash -lc 'cp "$SOURCE_FILE" src/runtime.ts'` },
23581
+ },
23582
+ { cwd },
23583
+ );
23584
+
23585
+ assert.equal(result.outputJson?.decision, "block");
23586
+ assert.match(String(result.outputJson?.reason ?? ""), /Bash nested shell execution is dynamic and cannot be validated/);
23587
+ } finally {
23588
+ await rm(cwd, { recursive: true, force: true });
23589
+ }
23590
+ });
23591
+
21887
23592
  it("blocks common Bash file mutations in Main-root conductor states unless they target workflow metadata", async () => {
21888
23593
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-bash-mutations-"));
21889
23594
  try {