oh-my-codex 0.18.11 → 0.18.13

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 (196) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -1
  4. package/dist/autopilot/__tests__/ralplan-gate.test.js +668 -0
  5. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  6. package/dist/autopilot/completion-gate.d.ts +10 -0
  7. package/dist/autopilot/completion-gate.d.ts.map +1 -0
  8. package/dist/autopilot/completion-gate.js +154 -0
  9. package/dist/autopilot/completion-gate.js.map +1 -0
  10. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  11. package/dist/autopilot/ralplan-gate.js +42 -21
  12. package/dist/autopilot/ralplan-gate.js.map +1 -1
  13. package/dist/cli/__tests__/codex-plugin-layout.test.js +46 -3
  14. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  15. package/dist/cli/__tests__/doctor-invalid-config.test.js +35 -0
  16. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  17. package/dist/cli/__tests__/doctor-warning-copy.test.js +317 -0
  18. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  19. package/dist/cli/__tests__/index.test.js +120 -2
  20. package/dist/cli/__tests__/index.test.js.map +1 -1
  21. package/dist/cli/__tests__/launch-fallback.test.js +1 -1
  22. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  23. package/dist/cli/__tests__/resume.test.js +217 -1
  24. package/dist/cli/__tests__/resume.test.js.map +1 -1
  25. package/dist/cli/__tests__/session-scoped-runtime.test.js +101 -0
  26. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  27. package/dist/cli/__tests__/session-search-help.test.js +3 -2
  28. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  29. package/dist/cli/__tests__/session-search.test.js +64 -2
  30. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  31. package/dist/cli/__tests__/setup-agents-overwrite.test.js +289 -1
  32. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  33. package/dist/cli/__tests__/setup-install-mode.test.js +290 -17
  34. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  35. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +74 -0
  36. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-scope.test.js +45 -0
  38. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  39. package/dist/cli/__tests__/state.test.js +93 -0
  40. package/dist/cli/__tests__/state.test.js.map +1 -1
  41. package/dist/cli/__tests__/update.test.js +157 -3
  42. package/dist/cli/__tests__/update.test.js.map +1 -1
  43. package/dist/cli/__tests__/version-sync-contract.test.js +2 -0
  44. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  45. package/dist/cli/doctor.d.ts.map +1 -1
  46. package/dist/cli/doctor.js +90 -12
  47. package/dist/cli/doctor.js.map +1 -1
  48. package/dist/cli/index.d.ts +13 -4
  49. package/dist/cli/index.d.ts.map +1 -1
  50. package/dist/cli/index.js +439 -46
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/cli/project-runtime-codex-homes.d.ts +6 -0
  53. package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -0
  54. package/dist/cli/project-runtime-codex-homes.js +27 -0
  55. package/dist/cli/project-runtime-codex-homes.js.map +1 -0
  56. package/dist/cli/session-search.d.ts.map +1 -1
  57. package/dist/cli/session-search.js +8 -1
  58. package/dist/cli/session-search.js.map +1 -1
  59. package/dist/cli/setup.d.ts +2 -2
  60. package/dist/cli/setup.d.ts.map +1 -1
  61. package/dist/cli/setup.js +482 -126
  62. package/dist/cli/setup.js.map +1 -1
  63. package/dist/cli/state.d.ts.map +1 -1
  64. package/dist/cli/state.js +79 -8
  65. package/dist/cli/state.js.map +1 -1
  66. package/dist/cli/update.d.ts +1 -0
  67. package/dist/cli/update.d.ts.map +1 -1
  68. package/dist/cli/update.js +42 -10
  69. package/dist/cli/update.js.map +1 -1
  70. package/dist/config/__tests__/codex-hooks.test.js +73 -29
  71. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  72. package/dist/config/codex-hooks.d.ts +14 -0
  73. package/dist/config/codex-hooks.d.ts.map +1 -1
  74. package/dist/config/codex-hooks.js +54 -51
  75. package/dist/config/codex-hooks.js.map +1 -1
  76. package/dist/config/generator.d.ts +1 -1
  77. package/dist/config/generator.d.ts.map +1 -1
  78. package/dist/config/generator.js +1 -1
  79. package/dist/config/generator.js.map +1 -1
  80. package/dist/hooks/__tests__/best-practice-research-skill.test.js +12 -0
  81. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -1
  82. package/dist/hud/__tests__/authority.test.js +45 -12
  83. package/dist/hud/__tests__/authority.test.js.map +1 -1
  84. package/dist/hud/__tests__/reconcile.test.js +95 -0
  85. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  86. package/dist/hud/__tests__/render.test.js +6 -6
  87. package/dist/hud/__tests__/render.test.js.map +1 -1
  88. package/dist/hud/__tests__/tmux.test.js +2 -2
  89. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  90. package/dist/hud/authority.d.ts.map +1 -1
  91. package/dist/hud/authority.js +17 -2
  92. package/dist/hud/authority.js.map +1 -1
  93. package/dist/hud/index.js +1 -4
  94. package/dist/hud/index.js.map +1 -1
  95. package/dist/hud/reconcile.d.ts.map +1 -1
  96. package/dist/hud/reconcile.js +42 -0
  97. package/dist/hud/reconcile.js.map +1 -1
  98. package/dist/hud/render.d.ts.map +1 -1
  99. package/dist/hud/render.js +6 -0
  100. package/dist/hud/render.js.map +1 -1
  101. package/dist/hud/tmux.d.ts.map +1 -1
  102. package/dist/hud/tmux.js +5 -4
  103. package/dist/hud/tmux.js.map +1 -1
  104. package/dist/mcp/__tests__/bootstrap.test.js +31 -1
  105. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  106. package/dist/mcp/bootstrap.d.ts +1 -0
  107. package/dist/mcp/bootstrap.d.ts.map +1 -1
  108. package/dist/mcp/bootstrap.js +32 -0
  109. package/dist/mcp/bootstrap.js.map +1 -1
  110. package/dist/modes/__tests__/base-autopilot-gates.test.d.ts +2 -0
  111. package/dist/modes/__tests__/base-autopilot-gates.test.d.ts.map +1 -0
  112. package/dist/modes/__tests__/base-autopilot-gates.test.js +154 -0
  113. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -0
  114. package/dist/modes/base.d.ts +4 -1
  115. package/dist/modes/base.d.ts.map +1 -1
  116. package/dist/modes/base.js +71 -1
  117. package/dist/modes/base.js.map +1 -1
  118. package/dist/pipeline/__tests__/orchestrator.test.js +144 -3
  119. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  120. package/dist/pipeline/__tests__/stages.test.js +109 -0
  121. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  122. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  123. package/dist/pipeline/orchestrator.js +11 -4
  124. package/dist/pipeline/orchestrator.js.map +1 -1
  125. package/dist/pipeline/stages/code-review.d.ts +2 -0
  126. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  127. package/dist/pipeline/stages/code-review.js +2 -0
  128. package/dist/pipeline/stages/code-review.js.map +1 -1
  129. package/dist/pipeline/stages/ultraqa.d.ts +3 -0
  130. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -1
  131. package/dist/pipeline/stages/ultraqa.js +3 -0
  132. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  133. package/dist/ralplan/__tests__/consensus-gate.test.d.ts +2 -0
  134. package/dist/ralplan/__tests__/consensus-gate.test.d.ts.map +1 -0
  135. package/dist/ralplan/__tests__/consensus-gate.test.js +631 -0
  136. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -0
  137. package/dist/ralplan/consensus-gate.d.ts +9 -1
  138. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  139. package/dist/ralplan/consensus-gate.js +287 -65
  140. package/dist/ralplan/consensus-gate.js.map +1 -1
  141. package/dist/scripts/__tests__/codex-native-hook.test.js +481 -0
  142. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  143. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  144. package/dist/scripts/codex-native-hook.js +145 -25
  145. package/dist/scripts/codex-native-hook.js.map +1 -1
  146. package/dist/scripts/codex-native-pre-post.d.ts +1 -0
  147. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  148. package/dist/scripts/codex-native-pre-post.js +130 -0
  149. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  150. package/dist/session-history/__tests__/search.test.js +166 -0
  151. package/dist/session-history/__tests__/search.test.js.map +1 -1
  152. package/dist/session-history/search.d.ts +7 -0
  153. package/dist/session-history/search.d.ts.map +1 -1
  154. package/dist/session-history/search.js +83 -24
  155. package/dist/session-history/search.js.map +1 -1
  156. package/dist/sidecar/__tests__/collector.test.js +60 -0
  157. package/dist/sidecar/__tests__/collector.test.js.map +1 -1
  158. package/dist/sidecar/collector.d.ts.map +1 -1
  159. package/dist/sidecar/collector.js +3 -6
  160. package/dist/sidecar/collector.js.map +1 -1
  161. package/dist/state/__tests__/operations.test.js +622 -0
  162. package/dist/state/__tests__/operations.test.js.map +1 -1
  163. package/dist/state/__tests__/skill-active.test.js +82 -0
  164. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  165. package/dist/state/operations.d.ts.map +1 -1
  166. package/dist/state/operations.js +31 -9
  167. package/dist/state/operations.js.map +1 -1
  168. package/dist/state/skill-active.d.ts.map +1 -1
  169. package/dist/state/skill-active.js +41 -1
  170. package/dist/state/skill-active.js.map +1 -1
  171. package/dist/team/__tests__/runtime.test.js +81 -57
  172. package/dist/team/__tests__/runtime.test.js.map +1 -1
  173. package/dist/team/runtime.js +4 -4
  174. package/dist/team/runtime.js.map +1 -1
  175. package/dist/utils/__tests__/paths.test.js +23 -0
  176. package/dist/utils/__tests__/paths.test.js.map +1 -1
  177. package/dist/utils/__tests__/version.test.js +27 -0
  178. package/dist/utils/__tests__/version.test.js.map +1 -1
  179. package/dist/utils/paths.d.ts.map +1 -1
  180. package/dist/utils/paths.js +4 -2
  181. package/dist/utils/paths.js.map +1 -1
  182. package/dist/utils/version.d.ts.map +1 -1
  183. package/dist/utils/version.js +7 -2
  184. package/dist/utils/version.js.map +1 -1
  185. package/dist/verification/__tests__/ci-rust-gates.test.js +4 -2
  186. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  187. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +71 -3
  188. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  189. package/package.json +1 -1
  190. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  191. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +53 -2
  192. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +6 -1
  193. package/skills/best-practice-research/SKILL.md +6 -1
  194. package/src/scripts/__tests__/codex-native-hook.test.ts +615 -0
  195. package/src/scripts/codex-native-hook.ts +162 -32
  196. package/src/scripts/codex-native-pre-post.ts +137 -0
@@ -1464,6 +1464,156 @@ describe("codex native hook dispatch", () => {
1464
1464
  }
1465
1465
  });
1466
1466
 
1467
+ it("suppresses child-agent SessionStart and Stop before the canonical leader session is reconciled (#2831)", async () => {
1468
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-no-canonical-"));
1469
+ const originalCodexHome = process.env.CODEX_HOME;
1470
+ try {
1471
+ process.env.CODEX_HOME = join(cwd, "codex-home");
1472
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
1473
+ notifications: {
1474
+ enabled: true,
1475
+ verbosity: "session",
1476
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
1477
+ },
1478
+ });
1479
+ const stateDir = join(cwd, ".omx", "state");
1480
+ const leaderNativeSessionId = "codex-leader-thread-no-canonical";
1481
+ const childNativeSessionId = "codex-child-thread-no-canonical";
1482
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
1483
+ await writeFile(
1484
+ join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
1485
+ [
1486
+ "import { appendFileSync } from 'node:fs';",
1487
+ "export async function onHookEvent(event) {",
1488
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
1489
+ "}",
1490
+ ].join("\n"),
1491
+ );
1492
+ const transcriptPath = join(cwd, "no-canonical-subagent-rollout.jsonl");
1493
+ await writeFile(
1494
+ transcriptPath,
1495
+ `${JSON.stringify({
1496
+ type: "session_meta",
1497
+ payload: {
1498
+ id: childNativeSessionId,
1499
+ source: {
1500
+ subagent: {
1501
+ thread_spawn: {
1502
+ parent_thread_id: leaderNativeSessionId,
1503
+ agent_role: "explorer",
1504
+ },
1505
+ },
1506
+ },
1507
+ },
1508
+ })}\n`,
1509
+ );
1510
+
1511
+ await dispatchCodexNativeHook(
1512
+ {
1513
+ hook_event_name: "SessionStart",
1514
+ cwd,
1515
+ session_id: childNativeSessionId,
1516
+ transcript_path: transcriptPath,
1517
+ },
1518
+ { cwd, sessionOwnerPid: process.pid },
1519
+ );
1520
+
1521
+ assert.equal(
1522
+ existsSync(join(cwd, "hook-events.jsonl")),
1523
+ false,
1524
+ "child SessionStart must be suppressed even before the canonical leader session is reconciled",
1525
+ );
1526
+ assert.equal(
1527
+ existsSync(join(stateDir, "session.json")),
1528
+ false,
1529
+ "child SessionStart must not be promoted into a root/leader session",
1530
+ );
1531
+
1532
+ const tracking = JSON.parse(
1533
+ await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
1534
+ ) as {
1535
+ sessions?: Record<string, {
1536
+ leader_thread_id?: string;
1537
+ threads?: Record<string, { kind?: string; mode?: string }>;
1538
+ }>;
1539
+ };
1540
+ assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
1541
+ assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
1542
+
1543
+ await dispatchCodexNativeHook(
1544
+ {
1545
+ hook_event_name: "Stop",
1546
+ cwd,
1547
+ session_id: childNativeSessionId,
1548
+ thread_id: childNativeSessionId,
1549
+ turn_id: "no-canonical-child-stop-turn",
1550
+ },
1551
+ { cwd },
1552
+ );
1553
+
1554
+ assert.equal(
1555
+ existsSync(join(cwd, "hook-events.jsonl")),
1556
+ false,
1557
+ "child Stop must be suppressed when the start was recognized as subagent-scoped",
1558
+ );
1559
+ } finally {
1560
+ if (originalCodexHome === undefined) {
1561
+ delete process.env.CODEX_HOME;
1562
+ } else {
1563
+ process.env.CODEX_HOME = originalCodexHome;
1564
+ }
1565
+ await rm(cwd, { recursive: true, force: true });
1566
+ }
1567
+ });
1568
+
1569
+ it("preserves root/leader SessionStart dispatch at session verbosity (#2831)", async () => {
1570
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-root-session-start-preserved-"));
1571
+ const originalCodexHome = process.env.CODEX_HOME;
1572
+ try {
1573
+ process.env.CODEX_HOME = join(cwd, "codex-home");
1574
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
1575
+ notifications: {
1576
+ enabled: true,
1577
+ verbosity: "session",
1578
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
1579
+ },
1580
+ });
1581
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
1582
+ await writeFile(
1583
+ join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
1584
+ [
1585
+ "import { appendFileSync } from 'node:fs';",
1586
+ "export async function onHookEvent(event) {",
1587
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
1588
+ "}",
1589
+ ].join("\n"),
1590
+ );
1591
+
1592
+ await dispatchCodexNativeHook(
1593
+ {
1594
+ hook_event_name: "SessionStart",
1595
+ cwd,
1596
+ session_id: "codex-root-thread-preserved",
1597
+ },
1598
+ { cwd, sessionOwnerPid: process.pid },
1599
+ );
1600
+
1601
+ const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
1602
+ assert.match(
1603
+ hookEvents,
1604
+ /"event":"session-start"/,
1605
+ "root/leader SessionStart must still dispatch at session verbosity",
1606
+ );
1607
+ } finally {
1608
+ if (originalCodexHome === undefined) {
1609
+ delete process.env.CODEX_HOME;
1610
+ } else {
1611
+ process.env.CODEX_HOME = originalCodexHome;
1612
+ }
1613
+ await rm(cwd, { recursive: true, force: true });
1614
+ }
1615
+ });
1616
+
1467
1617
  it("keeps a self-parented native role thread as subagent evidence", async () => {
1468
1618
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
1469
1619
  try {
@@ -5903,6 +6053,414 @@ exit 0
5903
6053
  }
5904
6054
  });
5905
6055
 
6056
+ it("allows read-only diagnostics mentioning apply_patch while deep-interview blocks real apply_patch invocations", async () => {
6057
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-grep-apply-patch-"));
6058
+ try {
6059
+ const stateDir = join(cwd, ".omx", "state");
6060
+ const sessionDir = join(stateDir, "sessions", "sess-di-grep");
6061
+ await mkdir(sessionDir, { recursive: true });
6062
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-grep", cwd });
6063
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6064
+ version: 1,
6065
+ active: true,
6066
+ skill: "deep-interview",
6067
+ phase: "planning",
6068
+ session_id: "sess-di-grep",
6069
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-grep" }],
6070
+ });
6071
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
6072
+ active: true,
6073
+ mode: "deep-interview",
6074
+ current_phase: "intent-first",
6075
+ session_id: "sess-di-grep",
6076
+ });
6077
+
6078
+ const allowedGrep = await dispatchCodexNativeHook(
6079
+ {
6080
+ hook_event_name: "PreToolUse",
6081
+ cwd,
6082
+ session_id: "sess-di-grep",
6083
+ tool_name: "Bash",
6084
+ tool_use_id: "tool-di-grep",
6085
+ tool_input: { command: 'grep -n "apply_patch" dist/scripts/codex-native-hook.js' },
6086
+ },
6087
+ { cwd },
6088
+ );
6089
+ assert.equal(allowedGrep.outputJson, null);
6090
+
6091
+ const allowedSubshellGrep = await dispatchCodexNativeHook(
6092
+ {
6093
+ hook_event_name: "PreToolUse",
6094
+ cwd,
6095
+ session_id: "sess-di-grep",
6096
+ tool_name: "Bash",
6097
+ tool_use_id: "tool-di-grep-subshell",
6098
+ tool_input: { command: '(grep -n "apply_patch" dist/scripts/codex-native-hook.js)' },
6099
+ },
6100
+ { cwd },
6101
+ );
6102
+ assert.equal(allowedSubshellGrep.outputJson, null);
6103
+
6104
+ // Double-quoted spans that merely mention the literal token, expand a
6105
+ // parameter, or run a substitution that is not `apply_patch` stay allowed
6106
+ // — the quoted-substitution fix must not over-block read-only diagnostics.
6107
+ const allowedQuotedMention = await dispatchCodexNativeHook(
6108
+ {
6109
+ hook_event_name: "PreToolUse",
6110
+ cwd,
6111
+ session_id: "sess-di-grep",
6112
+ tool_name: "Bash",
6113
+ tool_use_id: "tool-di-grep-quoted-mention",
6114
+ tool_input: { command: 'echo "${apply_patch} $(echo apply_patch)"' },
6115
+ },
6116
+ { cwd },
6117
+ );
6118
+ assert.equal(allowedQuotedMention.outputJson, null);
6119
+
6120
+ const blockedApplyPatch = await dispatchCodexNativeHook(
6121
+ {
6122
+ hook_event_name: "PreToolUse",
6123
+ cwd,
6124
+ session_id: "sess-di-grep",
6125
+ tool_name: "Bash",
6126
+ tool_use_id: "tool-di-apply-patch-invoke",
6127
+ tool_input: { command: "apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
6128
+ },
6129
+ { cwd },
6130
+ );
6131
+ assert.equal((blockedApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
6132
+
6133
+ const blockedEnvAssignmentApplyPatch = await dispatchCodexNativeHook(
6134
+ {
6135
+ hook_event_name: "PreToolUse",
6136
+ cwd,
6137
+ session_id: "sess-di-grep",
6138
+ tool_name: "Bash",
6139
+ tool_use_id: "tool-di-apply-patch-env-assignment",
6140
+ tool_input: { command: "FOO=bar apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
6141
+ },
6142
+ { cwd },
6143
+ );
6144
+ assert.equal((blockedEnvAssignmentApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
6145
+
6146
+ const blockedEnvWrapperApplyPatch = await dispatchCodexNativeHook(
6147
+ {
6148
+ hook_event_name: "PreToolUse",
6149
+ cwd,
6150
+ session_id: "sess-di-grep",
6151
+ tool_name: "Bash",
6152
+ tool_use_id: "tool-di-apply-patch-env-wrapper",
6153
+ tool_input: { command: "env FOO=bar apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
6154
+ },
6155
+ { cwd },
6156
+ );
6157
+ assert.equal((blockedEnvWrapperApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
6158
+
6159
+ const heredocBody = "\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF";
6160
+ const blockedRealApplyPatchForms: Array<{ id: string; command: string }> = [
6161
+ { id: "tool-di-apply-patch-env-i", command: `env -i apply_patch <<'EOF'${heredocBody}` },
6162
+ { id: "tool-di-apply-patch-env-unset", command: `env -u FOO apply_patch <<'EOF'${heredocBody}` },
6163
+ { id: "tool-di-apply-patch-env-i-assignment", command: `env -i FOO=bar apply_patch <<'EOF'${heredocBody}` },
6164
+ { id: "tool-di-apply-patch-assignment-env", command: `FOO=bar env apply_patch <<'EOF'${heredocBody}` },
6165
+ { id: "tool-di-apply-patch-exec-env-assignment", command: `exec env FOO=bar apply_patch <<'EOF'${heredocBody}` },
6166
+ { id: "tool-di-apply-patch-absolute-path", command: `/usr/bin/apply_patch <<'EOF'${heredocBody}` },
6167
+ { id: "tool-di-apply-patch-relative-path", command: `./apply_patch <<'EOF'${heredocBody}` },
6168
+ { id: "tool-di-apply-patch-subshell", command: `(apply_patch <<'EOF'${heredocBody}\n)` },
6169
+ { id: "tool-di-apply-patch-subshell-spaced", command: `( apply_patch <<'EOF'${heredocBody}\n)` },
6170
+ { id: "tool-di-apply-patch-double-subshell", command: `((apply_patch <<'EOF'${heredocBody}\n))` },
6171
+ { id: "tool-di-apply-patch-pipe-subshell", command: `true | (apply_patch <<'EOF'${heredocBody}\n)` },
6172
+ { id: "tool-di-apply-patch-subshell-env", command: `(env apply_patch <<'EOF'${heredocBody}\n)` },
6173
+ { id: "tool-di-apply-patch-command-substitution", command: `x=$(apply_patch <<'EOF'${heredocBody}\n)` },
6174
+ { id: "tool-di-apply-patch-brace-group", command: `{ apply_patch <<'EOF'${heredocBody}\n}` },
6175
+ // Command substitution runs even inside double quotes, so quoting the
6176
+ // already-blocked `$(…)` / `` `…` `` form must not bypass the guard.
6177
+ { id: "tool-di-apply-patch-quoted-command-substitution", command: `echo "$(apply_patch <<'EOF'${heredocBody}\n)"` },
6178
+ { id: "tool-di-apply-patch-quoted-backtick", command: `echo "\`apply_patch <<'EOF'${heredocBody}\`"` },
6179
+ { id: "tool-di-apply-patch-quoted-command-substitution-prefixed", command: `echo "patched: $(apply_patch <<'EOF'${heredocBody}\n) done"` },
6180
+ ];
6181
+ for (const form of blockedRealApplyPatchForms) {
6182
+ const blockedForm = await dispatchCodexNativeHook(
6183
+ {
6184
+ hook_event_name: "PreToolUse",
6185
+ cwd,
6186
+ session_id: "sess-di-grep",
6187
+ tool_name: "Bash",
6188
+ tool_use_id: form.id,
6189
+ tool_input: { command: form.command },
6190
+ },
6191
+ { cwd },
6192
+ );
6193
+ assert.equal(
6194
+ (blockedForm.outputJson as { decision?: string } | null)?.decision,
6195
+ "block",
6196
+ `expected deep-interview to block real apply_patch form: ${form.command}`,
6197
+ );
6198
+ }
6199
+ } finally {
6200
+ await rm(cwd, { recursive: true, force: true });
6201
+ }
6202
+ });
6203
+
6204
+ it("allows deep-interview same-command literal variable redirects to artifacts while blocking variable redirects outside them", async () => {
6205
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-var-redirect-"));
6206
+ try {
6207
+ const stateDir = join(cwd, ".omx", "state");
6208
+ const sessionDir = join(stateDir, "sessions", "sess-di-var-redirect");
6209
+ await mkdir(sessionDir, { recursive: true });
6210
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-var-redirect", cwd });
6211
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6212
+ version: 1,
6213
+ active: true,
6214
+ skill: "deep-interview",
6215
+ phase: "planning",
6216
+ session_id: "sess-di-var-redirect",
6217
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-var-redirect" }],
6218
+ });
6219
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
6220
+ active: true,
6221
+ mode: "deep-interview",
6222
+ current_phase: "intent-first",
6223
+ session_id: "sess-di-var-redirect",
6224
+ });
6225
+
6226
+ const allowedVarRedirect = await dispatchCodexNativeHook(
6227
+ {
6228
+ hook_event_name: "PreToolUse",
6229
+ cwd,
6230
+ session_id: "sess-di-var-redirect",
6231
+ tool_name: "Bash",
6232
+ tool_use_id: "tool-di-var-redirect-allow",
6233
+ tool_input: { command: 'SNAP=".omx/context/example.md"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
6234
+ },
6235
+ { cwd },
6236
+ );
6237
+ assert.equal(allowedVarRedirect.outputJson, null);
6238
+
6239
+ const blockedVarRedirect = await dispatchCodexNativeHook(
6240
+ {
6241
+ hook_event_name: "PreToolUse",
6242
+ cwd,
6243
+ session_id: "sess-di-var-redirect",
6244
+ tool_name: "Bash",
6245
+ tool_use_id: "tool-di-var-redirect-block",
6246
+ tool_input: { command: 'SNAP="src/leak.ts"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
6247
+ },
6248
+ { cwd },
6249
+ );
6250
+ assert.equal((blockedVarRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6251
+
6252
+ const blockedUnresolvedVarRedirect = await dispatchCodexNativeHook(
6253
+ {
6254
+ hook_event_name: "PreToolUse",
6255
+ cwd,
6256
+ session_id: "sess-di-var-redirect",
6257
+ tool_name: "Bash",
6258
+ tool_use_id: "tool-di-var-redirect-unresolved",
6259
+ tool_input: { command: 'cat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
6260
+ },
6261
+ { cwd },
6262
+ );
6263
+ assert.equal((blockedUnresolvedVarRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6264
+ } finally {
6265
+ await rm(cwd, { recursive: true, force: true });
6266
+ }
6267
+ });
6268
+
6269
+ it("allows deep-interview apply_patch artifact writes from freeform patch text while blocking outside paths", async () => {
6270
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-apply-patch-"));
6271
+ try {
6272
+ const stateDir = join(cwd, ".omx", "state");
6273
+ const sessionDir = join(stateDir, "sessions", "sess-di-apply-patch");
6274
+ await mkdir(sessionDir, { recursive: true });
6275
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-apply-patch", cwd });
6276
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6277
+ version: 1,
6278
+ active: true,
6279
+ skill: "deep-interview",
6280
+ phase: "planning",
6281
+ session_id: "sess-di-apply-patch",
6282
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-apply-patch" }],
6283
+ });
6284
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
6285
+ active: true,
6286
+ mode: "deep-interview",
6287
+ current_phase: "intent-first",
6288
+ session_id: "sess-di-apply-patch",
6289
+ });
6290
+
6291
+ const allowedAddFile = await dispatchCodexNativeHook(
6292
+ {
6293
+ hook_event_name: "PreToolUse",
6294
+ cwd,
6295
+ session_id: "sess-di-apply-patch",
6296
+ tool_name: "apply_patch",
6297
+ tool_use_id: "tool-di-apply-patch-add",
6298
+ tool_input: {
6299
+ input: "*** Begin Patch\n*** Add File: .omx/context/findings.md\n+# Findings\n*** End Patch\n",
6300
+ },
6301
+ },
6302
+ { cwd },
6303
+ );
6304
+ assert.equal(allowedAddFile.outputJson, null);
6305
+
6306
+ const allowedUpdateFile = await dispatchCodexNativeHook(
6307
+ {
6308
+ hook_event_name: "PreToolUse",
6309
+ cwd,
6310
+ session_id: "sess-di-apply-patch",
6311
+ tool_name: "ApplyPatch",
6312
+ tool_use_id: "tool-di-apply-patch-update",
6313
+ tool_input: {
6314
+ input: "*** Begin Patch\n*** Update File: .omx/specs/deep-interview-demo.md\n@@\n-old\n+new\n*** End Patch\n",
6315
+ },
6316
+ },
6317
+ { cwd },
6318
+ );
6319
+ assert.equal(allowedUpdateFile.outputJson, null);
6320
+
6321
+ const allowedStateWrite = await dispatchCodexNativeHook(
6322
+ {
6323
+ hook_event_name: "PreToolUse",
6324
+ cwd,
6325
+ session_id: "sess-di-apply-patch",
6326
+ tool_name: "apply_patch",
6327
+ tool_use_id: "tool-di-apply-patch-state",
6328
+ tool_input: {
6329
+ input: "*** Begin Patch\n*** Add File: .omx/state/deep-interview-notes.json\n+{}\n*** End Patch\n",
6330
+ },
6331
+ },
6332
+ { cwd },
6333
+ );
6334
+ assert.equal(allowedStateWrite.outputJson, null);
6335
+
6336
+ const blockedOutsidePath = await dispatchCodexNativeHook(
6337
+ {
6338
+ hook_event_name: "PreToolUse",
6339
+ cwd,
6340
+ session_id: "sess-di-apply-patch",
6341
+ tool_name: "apply_patch",
6342
+ tool_use_id: "tool-di-apply-patch-outside",
6343
+ tool_input: {
6344
+ input: "*** Begin Patch\n*** Add File: src/implementation.ts\n+export const x = 1;\n*** End Patch\n",
6345
+ },
6346
+ },
6347
+ { cwd },
6348
+ );
6349
+ assert.equal((blockedOutsidePath.outputJson as { decision?: string } | null)?.decision, "block");
6350
+ assert.match(String((blockedOutsidePath.outputJson as { reason?: string } | null)?.reason ?? ""), /Deep-interview is active/);
6351
+
6352
+ const blockedMixedPaths = await dispatchCodexNativeHook(
6353
+ {
6354
+ hook_event_name: "PreToolUse",
6355
+ cwd,
6356
+ session_id: "sess-di-apply-patch",
6357
+ tool_name: "apply_patch",
6358
+ tool_use_id: "tool-di-apply-patch-mixed",
6359
+ tool_input: {
6360
+ input: "*** Begin Patch\n*** Add File: .omx/context/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
6361
+ },
6362
+ },
6363
+ { cwd },
6364
+ );
6365
+ assert.equal((blockedMixedPaths.outputJson as { decision?: string } | null)?.decision, "block");
6366
+
6367
+ const blockedUnparseablePatch = await dispatchCodexNativeHook(
6368
+ {
6369
+ hook_event_name: "PreToolUse",
6370
+ cwd,
6371
+ session_id: "sess-di-apply-patch",
6372
+ tool_name: "apply_patch",
6373
+ tool_use_id: "tool-di-apply-patch-empty",
6374
+ tool_input: { input: "not a recognizable patch" },
6375
+ },
6376
+ { cwd },
6377
+ );
6378
+ assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
6379
+ } finally {
6380
+ await rm(cwd, { recursive: true, force: true });
6381
+ }
6382
+ });
6383
+
6384
+ it("allows Autopilot ralplan planning artifacts while blocking implementation writes", async () => {
6385
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-ralplan-artifact-"));
6386
+ try {
6387
+ const stateDir = join(cwd, ".omx", "state");
6388
+ const sessionDir = join(stateDir, "sessions", "sess-autopilot-ralplan-artifact");
6389
+ await mkdir(sessionDir, { recursive: true });
6390
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-ralplan-artifact", cwd });
6391
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6392
+ version: 1,
6393
+ active: true,
6394
+ skill: "autopilot",
6395
+ phase: "ralplan",
6396
+ session_id: "sess-autopilot-ralplan-artifact",
6397
+ thread_id: "thread-autopilot-ralplan-artifact",
6398
+ active_skills: [
6399
+ {
6400
+ skill: "autopilot",
6401
+ phase: "ralplan",
6402
+ active: true,
6403
+ session_id: "sess-autopilot-ralplan-artifact",
6404
+ thread_id: "thread-autopilot-ralplan-artifact",
6405
+ },
6406
+ ],
6407
+ });
6408
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
6409
+ active: true,
6410
+ mode: "autopilot",
6411
+ current_phase: "ralplan",
6412
+ session_id: "sess-autopilot-ralplan-artifact",
6413
+ thread_id: "thread-autopilot-ralplan-artifact",
6414
+ });
6415
+
6416
+ const allowedPlanWrite = await dispatchCodexNativeHook(
6417
+ {
6418
+ hook_event_name: "PreToolUse",
6419
+ cwd,
6420
+ session_id: "sess-autopilot-ralplan-artifact",
6421
+ thread_id: "thread-autopilot-ralplan-artifact",
6422
+ tool_name: "Write",
6423
+ tool_use_id: "tool-autopilot-ralplan-plan-write",
6424
+ tool_input: { file_path: ".omx/plans/prd-omx-y7a.md", content: "# Plan" },
6425
+ },
6426
+ { cwd },
6427
+ );
6428
+ assert.equal(allowedPlanWrite.outputJson, null);
6429
+
6430
+ const allowedSpecEdit = await dispatchCodexNativeHook(
6431
+ {
6432
+ hook_event_name: "PreToolUse",
6433
+ cwd,
6434
+ session_id: "sess-autopilot-ralplan-artifact",
6435
+ thread_id: "thread-autopilot-ralplan-artifact",
6436
+ tool_name: "Edit",
6437
+ tool_use_id: "tool-autopilot-ralplan-spec-edit",
6438
+ tool_input: { file_path: ".omx/specs/omx-y7a.md", old_string: "old", new_string: "new" },
6439
+ },
6440
+ { cwd },
6441
+ );
6442
+ assert.equal(allowedSpecEdit.outputJson, null);
6443
+
6444
+ const blockedImplementationEdit = await dispatchCodexNativeHook(
6445
+ {
6446
+ hook_event_name: "PreToolUse",
6447
+ cwd,
6448
+ session_id: "sess-autopilot-ralplan-artifact",
6449
+ thread_id: "thread-autopilot-ralplan-artifact",
6450
+ tool_name: "Edit",
6451
+ tool_use_id: "tool-autopilot-ralplan-src-edit",
6452
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
6453
+ },
6454
+ { cwd },
6455
+ );
6456
+ assert.equal((blockedImplementationEdit.outputJson as { decision?: string } | null)?.decision, "block");
6457
+ assert.match(String((blockedImplementationEdit.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/implementation\.ts/);
6458
+ assert.match(String((blockedImplementationEdit.outputJson as { reason?: string } | null)?.reason ?? ""), /not under allowed planning artifact paths/);
6459
+ } finally {
6460
+ await rm(cwd, { recursive: true, force: true });
6461
+ }
6462
+ });
6463
+
5906
6464
  it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
5907
6465
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
5908
6466
  try {
@@ -14121,6 +14679,63 @@ exit 0
14121
14679
  }
14122
14680
  });
14123
14681
 
14682
+ it("blocks implementation writes while Autopilot is supervising deep-interview without a persisted phase transition", async () => {
14683
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-deep-interview-pretool-block-"));
14684
+ try {
14685
+ const stateDir = join(cwd, ".omx", "state");
14686
+ const sessionId = "sess-autopilot-deep-interview-pretool-block";
14687
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
14688
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
14689
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
14690
+ active: true,
14691
+ skill: "autopilot",
14692
+ phase: "deep-interview",
14693
+ session_id: sessionId,
14694
+ active_skills: [{ skill: "autopilot", phase: "deep-interview", active: true, session_id: sessionId }],
14695
+ });
14696
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
14697
+ active: true,
14698
+ mode: "autopilot",
14699
+ current_phase: "deep-interview",
14700
+ session_id: sessionId,
14701
+ handoff_artifacts: {
14702
+ deep_interview: null,
14703
+ ralplan: null,
14704
+ ultragoal: null,
14705
+ code_review: null,
14706
+ ultraqa: null,
14707
+ },
14708
+ ralplan_consensus_gate: {
14709
+ ralplan_architect_review: null,
14710
+ ralplan_critic_review: null,
14711
+ complete: false,
14712
+ },
14713
+ });
14714
+
14715
+ const result = await dispatchCodexNativeHook(
14716
+ {
14717
+ hook_event_name: "PreToolUse",
14718
+ cwd,
14719
+ session_id: sessionId,
14720
+ thread_id: "thread-autopilot-deep-interview-pretool-block",
14721
+ tool_name: "Edit",
14722
+ tool_input: { file_path: "src/runtime.ts", old_string: "a", new_string: "b" },
14723
+ },
14724
+ { cwd },
14725
+ );
14726
+
14727
+ assert.equal(result.omxEventName, "pre-tool-use");
14728
+ assert.equal(result.outputJson?.decision, "block");
14729
+ assert.match(String(result.outputJson?.reason ?? ""), /Deep-interview is active .*implementation\/write tools are blocked/i);
14730
+ assert.match(
14731
+ String((result.outputJson?.hookSpecificOutput as { additionalContext?: string } | undefined)?.additionalContext ?? ""),
14732
+ /To implement, first ask for or process an explicit transition/i,
14733
+ );
14734
+ } finally {
14735
+ await rm(cwd, { recursive: true, force: true });
14736
+ }
14737
+ });
14738
+
14124
14739
  it("blocks implementation writes while Autopilot is supervising ralplan without handoff", async () => {
14125
14740
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-block-"));
14126
14741
  try {