oh-my-codex 0.18.2 → 0.18.4

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 (233) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +1 -0
  4. package/dist/agents/__tests__/definitions.test.js +9 -0
  5. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  6. package/dist/agents/__tests__/native-config.test.js +1 -0
  7. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  8. package/dist/agents/definitions.d.ts.map +1 -1
  9. package/dist/agents/definitions.js +10 -0
  10. package/dist/agents/definitions.js.map +1 -1
  11. package/dist/auth/__tests__/config-sessions.test.d.ts +2 -0
  12. package/dist/auth/__tests__/config-sessions.test.d.ts.map +1 -0
  13. package/dist/auth/__tests__/config-sessions.test.js +48 -0
  14. package/dist/auth/__tests__/config-sessions.test.js.map +1 -0
  15. package/dist/auth/__tests__/quota-rotation.test.d.ts +2 -0
  16. package/dist/auth/__tests__/quota-rotation.test.d.ts.map +1 -0
  17. package/dist/auth/__tests__/quota-rotation.test.js +33 -0
  18. package/dist/auth/__tests__/quota-rotation.test.js.map +1 -0
  19. package/dist/auth/__tests__/redact.test.d.ts +2 -0
  20. package/dist/auth/__tests__/redact.test.d.ts.map +1 -0
  21. package/dist/auth/__tests__/redact.test.js +20 -0
  22. package/dist/auth/__tests__/redact.test.js.map +1 -0
  23. package/dist/auth/__tests__/storage.test.d.ts +2 -0
  24. package/dist/auth/__tests__/storage.test.d.ts.map +1 -0
  25. package/dist/auth/__tests__/storage.test.js +108 -0
  26. package/dist/auth/__tests__/storage.test.js.map +1 -0
  27. package/dist/auth/config.d.ts +9 -0
  28. package/dist/auth/config.d.ts.map +1 -0
  29. package/dist/auth/config.js +77 -0
  30. package/dist/auth/config.js.map +1 -0
  31. package/dist/auth/hotswap.d.ts +36 -0
  32. package/dist/auth/hotswap.d.ts.map +1 -0
  33. package/dist/auth/hotswap.js +159 -0
  34. package/dist/auth/hotswap.js.map +1 -0
  35. package/dist/auth/index.d.ts +8 -0
  36. package/dist/auth/index.d.ts.map +1 -0
  37. package/dist/auth/index.js +8 -0
  38. package/dist/auth/index.js.map +1 -0
  39. package/dist/auth/paths.d.ts +12 -0
  40. package/dist/auth/paths.d.ts.map +1 -0
  41. package/dist/auth/paths.js +78 -0
  42. package/dist/auth/paths.js.map +1 -0
  43. package/dist/auth/quota-detector.d.ts +10 -0
  44. package/dist/auth/quota-detector.d.ts.map +1 -0
  45. package/dist/auth/quota-detector.js +40 -0
  46. package/dist/auth/quota-detector.js.map +1 -0
  47. package/dist/auth/redact.d.ts +2 -0
  48. package/dist/auth/redact.d.ts.map +1 -0
  49. package/dist/auth/redact.js +26 -0
  50. package/dist/auth/redact.js.map +1 -0
  51. package/dist/auth/rotation.d.ts +9 -0
  52. package/dist/auth/rotation.d.ts.map +1 -0
  53. package/dist/auth/rotation.js +26 -0
  54. package/dist/auth/rotation.js.map +1 -0
  55. package/dist/auth/sessions.d.ts +15 -0
  56. package/dist/auth/sessions.d.ts.map +1 -0
  57. package/dist/auth/sessions.js +62 -0
  58. package/dist/auth/sessions.js.map +1 -0
  59. package/dist/auth/storage.d.ts +27 -0
  60. package/dist/auth/storage.d.ts.map +1 -0
  61. package/dist/auth/storage.js +111 -0
  62. package/dist/auth/storage.js.map +1 -0
  63. package/dist/cli/__tests__/auth.test.d.ts +2 -0
  64. package/dist/cli/__tests__/auth.test.d.ts.map +1 -0
  65. package/dist/cli/__tests__/auth.test.js +168 -0
  66. package/dist/cli/__tests__/auth.test.js.map +1 -0
  67. package/dist/cli/__tests__/doctor-warning-copy.test.js +88 -3
  68. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  69. package/dist/cli/__tests__/explore.test.js +28 -7
  70. package/dist/cli/__tests__/explore.test.js.map +1 -1
  71. package/dist/cli/__tests__/index.test.js +70 -2
  72. package/dist/cli/__tests__/index.test.js.map +1 -1
  73. package/dist/cli/__tests__/nested-help-routing.test.js +1 -0
  74. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  75. package/dist/cli/__tests__/setup-agents-overwrite.test.js +30 -1
  76. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  77. package/dist/cli/__tests__/setup-install-mode.test.js +103 -17
  78. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  79. package/dist/cli/__tests__/setup-scope.test.js +1 -1
  80. package/dist/cli/__tests__/sparkshell-cli.test.js +2 -2
  81. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  82. package/dist/cli/auth.d.ts +4 -0
  83. package/dist/cli/auth.d.ts.map +1 -0
  84. package/dist/cli/auth.js +89 -0
  85. package/dist/cli/auth.js.map +1 -0
  86. package/dist/cli/doctor.d.ts.map +1 -1
  87. package/dist/cli/doctor.js +128 -19
  88. package/dist/cli/doctor.js.map +1 -1
  89. package/dist/cli/explore.d.ts +1 -0
  90. package/dist/cli/explore.d.ts.map +1 -1
  91. package/dist/cli/explore.js +18 -0
  92. package/dist/cli/explore.js.map +1 -1
  93. package/dist/cli/index.d.ts +20 -2
  94. package/dist/cli/index.d.ts.map +1 -1
  95. package/dist/cli/index.js +114 -10
  96. package/dist/cli/index.js.map +1 -1
  97. package/dist/cli/question.d.ts.map +1 -1
  98. package/dist/cli/question.js +5 -1
  99. package/dist/cli/question.js.map +1 -1
  100. package/dist/cli/setup.d.ts.map +1 -1
  101. package/dist/cli/setup.js +29 -57
  102. package/dist/cli/setup.js.map +1 -1
  103. package/dist/config/__tests__/deep-interview.test.d.ts +2 -0
  104. package/dist/config/__tests__/deep-interview.test.d.ts.map +1 -0
  105. package/dist/config/__tests__/deep-interview.test.js +239 -0
  106. package/dist/config/__tests__/deep-interview.test.js.map +1 -0
  107. package/dist/config/__tests__/generator-idempotent.test.js +128 -5
  108. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  109. package/dist/config/deep-interview.d.ts +22 -0
  110. package/dist/config/deep-interview.d.ts.map +1 -0
  111. package/dist/config/deep-interview.js +151 -0
  112. package/dist/config/deep-interview.js.map +1 -0
  113. package/dist/config/generator.d.ts +13 -4
  114. package/dist/config/generator.d.ts.map +1 -1
  115. package/dist/config/generator.js +154 -40
  116. package/dist/config/generator.js.map +1 -1
  117. package/dist/hooks/__tests__/agents-overlay.test.js +9 -7
  118. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  119. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -1
  120. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  121. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -0
  122. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  123. package/dist/hooks/__tests__/explore-routing.test.js +10 -12
  124. package/dist/hooks/__tests__/explore-routing.test.js.map +1 -1
  125. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +13 -15
  126. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  127. package/dist/hooks/__tests__/keyword-detector.test.js +301 -0
  128. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  129. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +33 -0
  130. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  131. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +60 -0
  132. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  133. package/dist/hooks/deep-interview-config-instruction.d.ts +3 -0
  134. package/dist/hooks/deep-interview-config-instruction.d.ts.map +1 -0
  135. package/dist/hooks/deep-interview-config-instruction.js +47 -0
  136. package/dist/hooks/deep-interview-config-instruction.js.map +1 -0
  137. package/dist/hooks/explore-routing.d.ts.map +1 -1
  138. package/dist/hooks/explore-routing.js +8 -13
  139. package/dist/hooks/explore-routing.js.map +1 -1
  140. package/dist/hooks/keyword-detector.d.ts +5 -0
  141. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  142. package/dist/hooks/keyword-detector.js +52 -8
  143. package/dist/hooks/keyword-detector.js.map +1 -1
  144. package/dist/hud/__tests__/hud-tmux-injection.test.js +19 -14
  145. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  146. package/dist/hud/__tests__/reconcile.test.js +117 -9
  147. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  148. package/dist/hud/__tests__/tmux.test.js +103 -1
  149. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  150. package/dist/hud/index.d.ts +1 -1
  151. package/dist/hud/index.d.ts.map +1 -1
  152. package/dist/hud/index.js +24 -2
  153. package/dist/hud/index.js.map +1 -1
  154. package/dist/hud/reconcile.d.ts +1 -1
  155. package/dist/hud/reconcile.d.ts.map +1 -1
  156. package/dist/hud/reconcile.js +23 -0
  157. package/dist/hud/reconcile.js.map +1 -1
  158. package/dist/hud/tmux.d.ts +7 -0
  159. package/dist/hud/tmux.d.ts.map +1 -1
  160. package/dist/hud/tmux.js +46 -9
  161. package/dist/hud/tmux.js.map +1 -1
  162. package/dist/question/__tests__/deep-interview.test.js +80 -7
  163. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  164. package/dist/question/__tests__/policy.test.js +83 -9
  165. package/dist/question/__tests__/policy.test.js.map +1 -1
  166. package/dist/question/autopilot-wait.d.ts +10 -0
  167. package/dist/question/autopilot-wait.d.ts.map +1 -0
  168. package/dist/question/autopilot-wait.js +134 -0
  169. package/dist/question/autopilot-wait.js.map +1 -0
  170. package/dist/question/deep-interview.d.ts +2 -0
  171. package/dist/question/deep-interview.d.ts.map +1 -1
  172. package/dist/question/deep-interview.js +4 -0
  173. package/dist/question/deep-interview.js.map +1 -1
  174. package/dist/question/policy.d.ts +1 -0
  175. package/dist/question/policy.d.ts.map +1 -1
  176. package/dist/question/policy.js +19 -0
  177. package/dist/question/policy.js.map +1 -1
  178. package/dist/scripts/__tests__/codex-native-hook.test.js +718 -0
  179. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  180. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  181. package/dist/scripts/codex-native-hook.js +69 -5
  182. package/dist/scripts/codex-native-hook.js.map +1 -1
  183. package/dist/scripts/notify-hook.js +13 -0
  184. package/dist/scripts/notify-hook.js.map +1 -1
  185. package/dist/state/__tests__/planning-gate.test.d.ts +2 -0
  186. package/dist/state/__tests__/planning-gate.test.d.ts.map +1 -0
  187. package/dist/state/__tests__/planning-gate.test.js +219 -0
  188. package/dist/state/__tests__/planning-gate.test.js.map +1 -0
  189. package/dist/state/workflow-transition.d.ts +23 -0
  190. package/dist/state/workflow-transition.d.ts.map +1 -1
  191. package/dist/state/workflow-transition.js +63 -0
  192. package/dist/state/workflow-transition.js.map +1 -1
  193. package/dist/subagents/__tests__/tracker.test.js +69 -0
  194. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  195. package/dist/subagents/tracker.d.ts +5 -0
  196. package/dist/subagents/tracker.d.ts.map +1 -1
  197. package/dist/subagents/tracker.js +16 -0
  198. package/dist/subagents/tracker.js.map +1 -1
  199. package/dist/team/__tests__/tmux-session.test.js +86 -0
  200. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  201. package/dist/team/tmux-session.d.ts.map +1 -1
  202. package/dist/team/tmux-session.js +7 -0
  203. package/dist/team/tmux-session.js.map +1 -1
  204. package/dist/ultragoal/__tests__/artifacts.test.js +126 -0
  205. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  206. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  207. package/dist/ultragoal/artifacts.js +126 -8
  208. package/dist/ultragoal/artifacts.js.map +1 -1
  209. package/package.json +1 -1
  210. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  211. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +2 -2
  212. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +11 -1
  213. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +4 -4
  214. package/plugins/oh-my-codex/skills/plan/SKILL.md +5 -5
  215. package/plugins/oh-my-codex/skills/ralph/SKILL.md +1 -1
  216. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +10 -6
  217. package/prompts/executor.md +1 -1
  218. package/prompts/explore-harness.md +2 -2
  219. package/prompts/explore.md +1 -1
  220. package/prompts/planner.md +1 -1
  221. package/prompts/scholastic.md +11 -0
  222. package/prompts/sisyphus-lite.md +1 -1
  223. package/skills/autopilot/SKILL.md +2 -2
  224. package/skills/deep-interview/SKILL.md +11 -1
  225. package/skills/omx-setup/SKILL.md +4 -4
  226. package/skills/plan/SKILL.md +5 -5
  227. package/skills/ralph/SKILL.md +1 -1
  228. package/skills/ralplan/SKILL.md +10 -6
  229. package/src/scripts/__tests__/codex-native-hook.test.ts +853 -0
  230. package/src/scripts/codex-native-hook.ts +73 -3
  231. package/src/scripts/notify-hook.ts +15 -0
  232. package/templates/AGENTS.md +3 -3
  233. package/templates/catalog-manifest.json +5 -0
@@ -25,6 +25,7 @@ import { writeSessionStart } from "../../hooks/session.js";
25
25
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
26
26
  import { executeStateOperation } from "../../state/operations.js";
27
27
  import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
28
+ import { OMX_TMUX_HUD_LEADER_PANE_ENV } from "../../hud/tmux.js";
28
29
  import { readAllState } from "../../hud/state.js";
29
30
  import { renderHud } from "../../hud/render.js";
30
31
  import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
@@ -65,6 +66,39 @@ async function writeJson(path: string, value: unknown): Promise<void> {
65
66
  await writeFile(path, JSON.stringify(value, null, 2));
66
67
  }
67
68
 
69
+ async function setTeamPaneIds(
70
+ cwd: string,
71
+ teamName: string,
72
+ paneIds: { leaderPaneId: string; workerPaneIds: Record<string, string> },
73
+ ): Promise<void> {
74
+ for (const fileName of ["config.json", "manifest.v2.json"]) {
75
+ const filePath = join(cwd, ".omx", "state", "team", teamName, fileName);
76
+ const parsed = JSON.parse(await readFile(filePath, "utf-8")) as {
77
+ leader_pane_id?: string | null;
78
+ workers?: Array<{ name?: string; pane_id?: string | null }>;
79
+ };
80
+ parsed.leader_pane_id = paneIds.leaderPaneId;
81
+ parsed.workers = (parsed.workers ?? []).map((worker) => ({
82
+ ...worker,
83
+ pane_id: worker.name ? paneIds.workerPaneIds[worker.name] ?? worker.pane_id ?? null : worker.pane_id ?? null,
84
+ }));
85
+ await writeJson(filePath, parsed);
86
+ }
87
+ }
88
+
89
+ async function withIsolatedHome<T>(prefix: string, run: (homeDir: string) => Promise<T>): Promise<T> {
90
+ const homeDir = await mkdtemp(join(tmpdir(), `omx-native-hook-home-${prefix}-`));
91
+ const previousHome = process.env.HOME;
92
+ try {
93
+ process.env.HOME = homeDir;
94
+ return await run(homeDir);
95
+ } finally {
96
+ if (typeof previousHome === "string") process.env.HOME = previousHome;
97
+ else delete process.env.HOME;
98
+ await rm(homeDir, { recursive: true, force: true });
99
+ }
100
+ }
101
+
68
102
  async function withLoreGuardConfig<T>(
69
103
  value: string,
70
104
  prefix: string,
@@ -229,6 +263,7 @@ const DEFAULT_AUTO_NUDGE_RESPONSE =
229
263
 
230
264
  const TEAM_ENV_KEYS = [
231
265
  "OMX_TEAM_WORKER",
266
+ "OMX_TEAM_INTERNAL_WORKER",
232
267
  "OMX_TEAM_STATE_ROOT",
233
268
  "OMX_TEAM_LEADER_CWD",
234
269
  "OMX_SESSION_ID",
@@ -1620,6 +1655,405 @@ describe("codex native hook dispatch", () => {
1620
1655
  }
1621
1656
  });
1622
1657
 
1658
+ it("injects deep-interview config overrides into UserPromptSubmit developer context", async () => {
1659
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-"));
1660
+ try {
1661
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1662
+ await writeFile(
1663
+ join(cwd, ".omx", "config.toml"),
1664
+ `[omx.deepInterview]
1665
+ defaultProfile = "standard"
1666
+ standardThreshold = 0.05
1667
+ standardMaxRounds = 15
1668
+ enableChallengeModes = false
1669
+ `,
1670
+ );
1671
+
1672
+ const result = await dispatchCodexNativeHook(
1673
+ {
1674
+ hook_event_name: "UserPromptSubmit",
1675
+ cwd,
1676
+ session_id: "sess-deep-interview-config",
1677
+ thread_id: "thread-1",
1678
+ turn_id: "turn-1",
1679
+ prompt: "$deep-interview prove config reflection",
1680
+ },
1681
+ { cwd },
1682
+ );
1683
+
1684
+ assert.equal(result.omxEventName, "keyword-detector");
1685
+ assert.equal(result.skillState?.skill, "deep-interview");
1686
+ const serializedOutput = JSON.stringify(result.outputJson);
1687
+ assert.match(serializedOutput, /Deep-interview config override active/);
1688
+ assert.match(serializedOutput, /threshold=0\.05/);
1689
+ assert.match(serializedOutput, /max_rounds=15/);
1690
+ assert.match(serializedOutput, /enableChallengeModes=false/);
1691
+
1692
+ const modeState = JSON.parse(
1693
+ await readFile(join(cwd, ".omx", "state", "sessions", "sess-deep-interview-config", "deep-interview-state.json"), "utf-8"),
1694
+ ) as { threshold?: number; max_rounds?: number; profile?: string };
1695
+ assert.equal(modeState.profile, "standard");
1696
+ assert.equal(modeState.threshold, 0.05);
1697
+ assert.equal(modeState.max_rounds, 15);
1698
+ } finally {
1699
+ await rm(cwd, { recursive: true, force: true });
1700
+ }
1701
+ });
1702
+
1703
+ it("proves UserPromptSubmit context changes before and after adding deep-interview config", async () => {
1704
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-before-after-"));
1705
+ const sessionId = "sess-deep-interview-config-before-after";
1706
+ try {
1707
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1708
+
1709
+ const before = await withIsolatedHome("deep-interview-config-before-after", async () => (
1710
+ dispatchCodexNativeHook(
1711
+ {
1712
+ hook_event_name: "UserPromptSubmit",
1713
+ cwd,
1714
+ session_id: sessionId,
1715
+ thread_id: "thread-before-after",
1716
+ turn_id: "turn-before",
1717
+ prompt: "$deep-interview prove before config context",
1718
+ },
1719
+ { cwd },
1720
+ )
1721
+ ));
1722
+ const beforeOutput = JSON.stringify(before.outputJson);
1723
+ const beforeState = JSON.parse(
1724
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1725
+ ) as {
1726
+ deep_interview_config?: unknown;
1727
+ threshold?: number;
1728
+ max_rounds?: number;
1729
+ };
1730
+ assert.equal(before.skillState?.skill, "deep-interview");
1731
+ assert.doesNotMatch(beforeOutput, /Deep-interview config override active/);
1732
+ assert.equal(before.skillState?.deep_interview_config, undefined);
1733
+ assert.equal(beforeState.deep_interview_config, undefined);
1734
+ assert.equal(beforeState.threshold, undefined);
1735
+ assert.equal(beforeState.max_rounds, undefined);
1736
+
1737
+ await writeFile(
1738
+ join(cwd, ".omx", "config.toml"),
1739
+ `[omx.deepInterview]
1740
+ defaultProfile = "standard"
1741
+ standardThreshold = 0.05
1742
+ standardMaxRounds = 15
1743
+ `,
1744
+ );
1745
+
1746
+ const after = await dispatchCodexNativeHook(
1747
+ {
1748
+ hook_event_name: "UserPromptSubmit",
1749
+ cwd,
1750
+ session_id: sessionId,
1751
+ thread_id: "thread-before-after",
1752
+ turn_id: "turn-after",
1753
+ prompt: "$deep-interview prove after config context",
1754
+ },
1755
+ { cwd },
1756
+ );
1757
+ const afterOutput = JSON.stringify(after.outputJson);
1758
+ const afterState = JSON.parse(
1759
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1760
+ ) as {
1761
+ deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number };
1762
+ threshold?: number;
1763
+ max_rounds?: number;
1764
+ };
1765
+ assert.equal(after.skillState?.deep_interview_config?.profile, "standard");
1766
+ assert.match(afterOutput, /Deep-interview config override active/);
1767
+ assert.match(afterOutput, /threshold=0\.05/);
1768
+ assert.match(afterOutput, /max_rounds=15/);
1769
+ assert.equal(afterState.deep_interview_config?.profile, "standard");
1770
+ assert.equal(afterState.threshold, 0.05);
1771
+ assert.equal(afterState.max_rounds, 15);
1772
+ } finally {
1773
+ await rm(cwd, { recursive: true, force: true });
1774
+ }
1775
+ });
1776
+
1777
+ it("injects deep-interview config for mixed workflow prompts that defer execution modes", async () => {
1778
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-mixed-"));
1779
+ const sessionId = "sess-deep-interview-config-mixed";
1780
+ try {
1781
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1782
+ await writeFile(
1783
+ join(cwd, ".omx", "config.toml"),
1784
+ `[omx.deepInterview]
1785
+ defaultProfile = "deep"
1786
+ deepThreshold = 0.13
1787
+ deepMaxRounds = 21
1788
+ enableChallengeModes = false
1789
+ `,
1790
+ );
1791
+
1792
+ const result = await withIsolatedHome("deep-interview-config-mixed", async () => (
1793
+ dispatchCodexNativeHook(
1794
+ {
1795
+ hook_event_name: "UserPromptSubmit",
1796
+ cwd,
1797
+ session_id: sessionId,
1798
+ thread_id: "thread-mixed-config",
1799
+ turn_id: "turn-mixed-config",
1800
+ prompt: "$autopilot $deep-interview prove mixed config context",
1801
+ },
1802
+ { cwd },
1803
+ )
1804
+ ));
1805
+ const serializedOutput = JSON.stringify(result.outputJson);
1806
+ const modeState = JSON.parse(
1807
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1808
+ ) as {
1809
+ deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number; enableChallengeModes?: boolean };
1810
+ profile?: string;
1811
+ threshold?: number;
1812
+ max_rounds?: number;
1813
+ enable_challenge_modes?: boolean;
1814
+ };
1815
+
1816
+ assert.equal(result.skillState?.skill, "deep-interview");
1817
+ assert.deepEqual(result.skillState?.deferred_skills, ["autopilot"]);
1818
+ assert.equal(result.skillState?.deep_interview_config?.profile, "deep");
1819
+ assert.equal(result.skillState?.deep_interview_config?.threshold, 0.13);
1820
+ assert.equal(result.skillState?.deep_interview_config?.maxRounds, 21);
1821
+ assert.equal(result.skillState?.deep_interview_config?.enableChallengeModes, false);
1822
+ assert.match(serializedOutput, /Deep-interview config override active/);
1823
+ assert.match(serializedOutput, /profile=deep/);
1824
+ assert.match(serializedOutput, /threshold=0\.13/);
1825
+ assert.match(serializedOutput, /max_rounds=21/);
1826
+ assert.match(serializedOutput, /enableChallengeModes=false/);
1827
+ assert.equal(modeState.deep_interview_config?.profile, "deep");
1828
+ assert.equal(modeState.profile, "deep");
1829
+ assert.equal(modeState.threshold, 0.13);
1830
+ assert.equal(modeState.max_rounds, 21);
1831
+ assert.equal(modeState.enable_challenge_modes, false);
1832
+ } finally {
1833
+ await rm(cwd, { recursive: true, force: true });
1834
+ }
1835
+ });
1836
+
1837
+ it("keeps deep-interview config override context on continuation prompts", async () => {
1838
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-continuation-"));
1839
+ const sessionId = "sess-deep-interview-config-continuation";
1840
+ try {
1841
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1842
+ await writeFile(
1843
+ join(cwd, ".omx", "config.toml"),
1844
+ `[omx.deepInterview]
1845
+ defaultProfile = "standard"
1846
+ standardThreshold = 0.05
1847
+ standardMaxRounds = 15
1848
+ `,
1849
+ );
1850
+
1851
+ await dispatchCodexNativeHook(
1852
+ {
1853
+ hook_event_name: "UserPromptSubmit",
1854
+ cwd,
1855
+ session_id: sessionId,
1856
+ thread_id: "thread-continuation",
1857
+ turn_id: "turn-start",
1858
+ prompt: "$deep-interview prove config continuation",
1859
+ },
1860
+ { cwd },
1861
+ );
1862
+ const continued = await dispatchCodexNativeHook(
1863
+ {
1864
+ hook_event_name: "UserPromptSubmit",
1865
+ cwd,
1866
+ session_id: sessionId,
1867
+ thread_id: "thread-continuation",
1868
+ turn_id: "turn-continue",
1869
+ prompt: "continue",
1870
+ },
1871
+ { cwd },
1872
+ );
1873
+ const serializedOutput = JSON.stringify(continued.outputJson);
1874
+ const modeState = JSON.parse(
1875
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1876
+ ) as { threshold?: number; max_rounds?: number; profile?: string };
1877
+
1878
+ assert.equal(continued.skillState?.skill, "deep-interview");
1879
+ assert.match(serializedOutput, /Deep-interview config override active/);
1880
+ assert.match(serializedOutput, /threshold=0\.05/);
1881
+ assert.match(serializedOutput, /max_rounds=15/);
1882
+ assert.equal(modeState.profile, "standard");
1883
+ assert.equal(modeState.threshold, 0.05);
1884
+ assert.equal(modeState.max_rounds, 15);
1885
+ } finally {
1886
+ await rm(cwd, { recursive: true, force: true });
1887
+ }
1888
+ });
1889
+
1890
+ it("keeps explicit deep-interview profile flags reflected on continuation prompts", async () => {
1891
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-profile-continuation-"));
1892
+ const sessionId = "sess-deep-interview-config-profile-continuation";
1893
+ try {
1894
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1895
+ await writeFile(
1896
+ join(cwd, ".omx", "config.toml"),
1897
+ `[omx.deepInterview]
1898
+ defaultProfile = "standard"
1899
+ standardThreshold = 0.22
1900
+ standardMaxRounds = 13
1901
+ deepThreshold = 0.13
1902
+ deepMaxRounds = 21
1903
+ `,
1904
+ );
1905
+
1906
+ await dispatchCodexNativeHook(
1907
+ {
1908
+ hook_event_name: "UserPromptSubmit",
1909
+ cwd,
1910
+ session_id: sessionId,
1911
+ thread_id: "thread-profile-continuation",
1912
+ turn_id: "turn-start",
1913
+ prompt: "$deep-interview --deep prove explicit profile continuation",
1914
+ },
1915
+ { cwd },
1916
+ );
1917
+ const continued = await dispatchCodexNativeHook(
1918
+ {
1919
+ hook_event_name: "UserPromptSubmit",
1920
+ cwd,
1921
+ session_id: sessionId,
1922
+ thread_id: "thread-profile-continuation",
1923
+ turn_id: "turn-continue",
1924
+ prompt: "continue",
1925
+ },
1926
+ { cwd },
1927
+ );
1928
+ const serializedOutput = JSON.stringify(continued.outputJson);
1929
+ const modeState = JSON.parse(
1930
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1931
+ ) as { threshold?: number; max_rounds?: number; profile?: string; deep_interview_config?: { profile?: string } };
1932
+
1933
+ assert.equal(continued.skillState?.skill, "deep-interview");
1934
+ assert.equal(continued.skillState?.deep_interview_config?.profile, "deep");
1935
+ assert.match(serializedOutput, /Deep-interview config override active/);
1936
+ assert.match(serializedOutput, /profile=deep/);
1937
+ assert.match(serializedOutput, /threshold=0\.13/);
1938
+ assert.match(serializedOutput, /max_rounds=21/);
1939
+ assert.equal(modeState.deep_interview_config?.profile, "deep");
1940
+ assert.equal(modeState.profile, "deep");
1941
+ assert.equal(modeState.threshold, 0.13);
1942
+ assert.equal(modeState.max_rounds, 21);
1943
+ } finally {
1944
+ await rm(cwd, { recursive: true, force: true });
1945
+ }
1946
+ });
1947
+
1948
+ it("keeps the documented deep-interview Suggested Config reflected in UserPromptSubmit context", async () => {
1949
+ const skillDoc = await readFile(join(process.cwd(), "skills", "deep-interview", "SKILL.md"), "utf-8");
1950
+ const markerIndex = skillDoc.indexOf("## Suggested Config (optional)");
1951
+ assert.notEqual(markerIndex, -1);
1952
+ const configMatch = skillDoc.slice(markerIndex).match(/```toml\n([\s\S]*?)\n```/);
1953
+ assert.ok(configMatch);
1954
+ const documentedConfig = configMatch[1]?.trimEnd();
1955
+ assert.ok(documentedConfig);
1956
+ assert.match(documentedConfig, /standardThreshold = 0\.20/);
1957
+ assert.match(documentedConfig, /standardMaxRounds = 12/);
1958
+
1959
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-doc-config-"));
1960
+ const sessionId = "sess-deep-interview-doc-config";
1961
+ try {
1962
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1963
+ await writeFile(join(cwd, ".omx", "config.toml"), `${documentedConfig}\n`);
1964
+
1965
+ const result = await dispatchCodexNativeHook(
1966
+ {
1967
+ hook_event_name: "UserPromptSubmit",
1968
+ cwd,
1969
+ session_id: sessionId,
1970
+ thread_id: "thread-doc-config",
1971
+ turn_id: "turn-doc-config",
1972
+ prompt: "$deep-interview prove documented config context",
1973
+ },
1974
+ { cwd },
1975
+ );
1976
+ const serializedOutput = JSON.stringify(result.outputJson);
1977
+ const modeState = JSON.parse(
1978
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1979
+ ) as {
1980
+ deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number };
1981
+ profile?: string;
1982
+ threshold?: number;
1983
+ max_rounds?: number;
1984
+ };
1985
+
1986
+ assert.equal(result.skillState?.deep_interview_config?.profile, "standard");
1987
+ assert.equal(result.skillState?.deep_interview_config?.threshold, 0.2);
1988
+ assert.equal(result.skillState?.deep_interview_config?.maxRounds, 12);
1989
+ assert.match(serializedOutput, /Deep-interview config override active/);
1990
+ assert.match(serializedOutput, /profile=standard/);
1991
+ assert.match(serializedOutput, /threshold=0\.2/);
1992
+ assert.match(serializedOutput, /max_rounds=12/);
1993
+ assert.equal(modeState.deep_interview_config?.profile, "standard");
1994
+ assert.equal(modeState.profile, "standard");
1995
+ assert.equal(modeState.threshold, 0.2);
1996
+ assert.equal(modeState.max_rounds, 12);
1997
+ } finally {
1998
+ await rm(cwd, { recursive: true, force: true });
1999
+ }
2000
+ });
2001
+
2002
+ it("injects deep-interview config overrides when state is boxed under OMX_ROOT", async () => {
2003
+ const root = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-boxed-"));
2004
+ const cwd = join(root, "source");
2005
+ const omxRoot = join(root, "box");
2006
+ const sessionId = "sess-boxed-deep-interview-config";
2007
+ const previousOmxRoot = process.env.OMX_ROOT;
2008
+ const previousOmxStateRoot = process.env.OMX_STATE_ROOT;
2009
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
2010
+ try {
2011
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2012
+ await writeFile(
2013
+ join(cwd, ".omx", "config.toml"),
2014
+ `[omx.deepInterview]
2015
+ defaultProfile = "standard"
2016
+ standardThreshold = 0.05
2017
+ standardMaxRounds = 15
2018
+ `,
2019
+ );
2020
+ process.env.OMX_ROOT = omxRoot;
2021
+ delete process.env.OMX_STATE_ROOT;
2022
+ delete process.env.OMX_TEAM_STATE_ROOT;
2023
+
2024
+ const result = await dispatchCodexNativeHook(
2025
+ {
2026
+ hook_event_name: "UserPromptSubmit",
2027
+ cwd,
2028
+ session_id: sessionId,
2029
+ thread_id: "thread-boxed",
2030
+ turn_id: "turn-boxed",
2031
+ prompt: "$deep-interview prove boxed config reflection",
2032
+ },
2033
+ { cwd },
2034
+ );
2035
+
2036
+ assert.equal(result.omxEventName, "keyword-detector");
2037
+ assert.equal(result.skillState?.initialized_state_path, `.omx/state/sessions/${sessionId}/deep-interview-state.json`);
2038
+ const boxedStatePath = join(omxRoot, ".omx", "state", "sessions", sessionId, "deep-interview-state.json");
2039
+ assert.equal(existsSync(boxedStatePath), true);
2040
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json")), false);
2041
+
2042
+ const serializedOutput = JSON.stringify(result.outputJson);
2043
+ assert.match(serializedOutput, /Deep-interview config override active/);
2044
+ assert.match(serializedOutput, /threshold=0\.05/);
2045
+ assert.match(serializedOutput, /max_rounds=15/);
2046
+ } finally {
2047
+ if (typeof previousOmxRoot === "string") process.env.OMX_ROOT = previousOmxRoot;
2048
+ else delete process.env.OMX_ROOT;
2049
+ if (typeof previousOmxStateRoot === "string") process.env.OMX_STATE_ROOT = previousOmxStateRoot;
2050
+ else delete process.env.OMX_STATE_ROOT;
2051
+ if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
2052
+ else delete process.env.OMX_TEAM_STATE_ROOT;
2053
+ await rm(root, { recursive: true, force: true });
2054
+ }
2055
+ });
2056
+
1623
2057
  it("records boxed keyword activation mode detail and skill state under OMX_ROOT", async () => {
1624
2058
  const root = await mkdtemp(join(tmpdir(), "omx-native-hook-boxed-"));
1625
2059
  const cwd = join(root, "source");
@@ -1949,6 +2383,38 @@ describe("codex native hook dispatch", () => {
1949
2383
  }
1950
2384
  });
1951
2385
 
2386
+ it("does not repeat ultragoal Stop recovery after a safe completed-aggregate microgoal blocker is recorded", async () => {
2387
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-aggregate-blocked-stop-"));
2388
+ try {
2389
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
2390
+ version: 1,
2391
+ codexGoalMode: "aggregate",
2392
+ activeGoalId: "G001-demo",
2393
+ goals: [{
2394
+ id: "G001-demo",
2395
+ status: "in_progress",
2396
+ objective: "Demo goal",
2397
+ failureReason: "aggregate Codex goal already complete and unreconcilable while repo-native .omx/ultragoal/goals.json still has an in-progress microgoal; stop the recovery loop",
2398
+ }],
2399
+ });
2400
+
2401
+ const result = await dispatchCodexNativeHook({
2402
+ hook_event_name: "Stop",
2403
+ cwd,
2404
+ session_id: "sess-ultragoal-aggregate-blocked-stop",
2405
+ thread_id: "thread-ultragoal-aggregate-blocked-stop",
2406
+ stop_hook_active: true,
2407
+ last_assistant_message: "Goal complete.",
2408
+ }, { cwd });
2409
+
2410
+ assert.notEqual(result.outputJson?.decision, "block");
2411
+ assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
2412
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
2413
+ } finally {
2414
+ await rm(cwd, { recursive: true, force: true });
2415
+ }
2416
+ });
2417
+
1952
2418
 
1953
2419
  it("does not block ultragoal Stop after task-scoped reconciliation finishes exploded bookkeeping", async () => {
1954
2420
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
@@ -3439,6 +3905,198 @@ export async function onHookEvent(event) {
3439
3905
  }
3440
3906
  });
3441
3907
 
3908
+ it("skips prompt-submit HUD reconciliation for confirmed team worker panes", async () => {
3909
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-worker-skip-"));
3910
+ try {
3911
+ const teamName = "hud-worker-skip";
3912
+ await initTeamState(teamName, "skip worker HUD reconcile", "executor", 1, cwd);
3913
+ await setTeamPaneIds(cwd, teamName, {
3914
+ leaderPaneId: "%42",
3915
+ workerPaneIds: { "worker-1": "%10" },
3916
+ });
3917
+ process.env.TMUX = "1";
3918
+ process.env.TMUX_PANE = "%10";
3919
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
3920
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
3921
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3922
+
3923
+ let reconcileCalls = 0;
3924
+ const result = await dispatchCodexNativeHook(
3925
+ {
3926
+ hook_event_name: "UserPromptSubmit",
3927
+ cwd,
3928
+ session_id: "sess-hud-team-worker",
3929
+ prompt: "$ralplan prepare plan",
3930
+ },
3931
+ {
3932
+ cwd,
3933
+ reconcileHudForPromptSubmitFn: async () => {
3934
+ reconcileCalls += 1;
3935
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3936
+ },
3937
+ },
3938
+ );
3939
+
3940
+ assert.equal(result.omxEventName, "keyword-detector");
3941
+ assert.equal(reconcileCalls, 0);
3942
+ } finally {
3943
+ await rm(cwd, { recursive: true, force: true });
3944
+ }
3945
+ });
3946
+
3947
+ it("preserves prompt-submit HUD reconciliation for team leader panes", async () => {
3948
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-leader-preserve-"));
3949
+ try {
3950
+ const teamName = "hud-leader-keep";
3951
+ await initTeamState(teamName, "preserve leader HUD reconcile", "executor", 1, cwd);
3952
+ await setTeamPaneIds(cwd, teamName, {
3953
+ leaderPaneId: "%42",
3954
+ workerPaneIds: { "worker-1": "%10" },
3955
+ });
3956
+ process.env.TMUX = "1";
3957
+ process.env.TMUX_PANE = "%42";
3958
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3959
+
3960
+ let reconcileCall: { cwd: string; sessionId?: string } | null = null;
3961
+ const result = await dispatchCodexNativeHook(
3962
+ {
3963
+ hook_event_name: "UserPromptSubmit",
3964
+ cwd,
3965
+ session_id: "sess-hud-team-leader",
3966
+ prompt: "$ralplan prepare plan",
3967
+ },
3968
+ {
3969
+ cwd,
3970
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
3971
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
3972
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3973
+ },
3974
+ },
3975
+ );
3976
+
3977
+ assert.equal(result.omxEventName, "keyword-detector");
3978
+ assert.deepEqual(reconcileCall, { cwd, sessionId: "sess-hud-team-leader" });
3979
+ } finally {
3980
+ await rm(cwd, { recursive: true, force: true });
3981
+ }
3982
+ });
3983
+
3984
+ it("preserves prompt-submit HUD reconciliation when worker pane detection is ambiguous", async () => {
3985
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-worker-ambiguous-"));
3986
+ try {
3987
+ const teamName = "hud-worker-ambiguous";
3988
+ await initTeamState(teamName, "fail closed for ambiguous worker HUD reconcile", "executor", 1, cwd);
3989
+ await setTeamPaneIds(cwd, teamName, {
3990
+ leaderPaneId: "%42",
3991
+ workerPaneIds: { "worker-1": "%10" },
3992
+ });
3993
+ process.env.TMUX = "1";
3994
+ process.env.TMUX_PANE = "%99";
3995
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
3996
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
3997
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3998
+
3999
+ let reconcileCalls = 0;
4000
+ const result = await dispatchCodexNativeHook(
4001
+ {
4002
+ hook_event_name: "UserPromptSubmit",
4003
+ cwd,
4004
+ session_id: "sess-hud-team-worker-ambiguous",
4005
+ prompt: "$ralplan prepare plan",
4006
+ },
4007
+ {
4008
+ cwd,
4009
+ reconcileHudForPromptSubmitFn: async () => {
4010
+ reconcileCalls += 1;
4011
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
4012
+ },
4013
+ },
4014
+ );
4015
+
4016
+ assert.equal(result.omxEventName, "keyword-detector");
4017
+ assert.equal(reconcileCalls, 1);
4018
+ } finally {
4019
+ await rm(cwd, { recursive: true, force: true });
4020
+ }
4021
+ });
4022
+
4023
+ it("preserves prompt-submit HUD reconciliation for native subagents even with worker pane env", async () => {
4024
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-subagent-worker-preserve-"));
4025
+ try {
4026
+ const teamName = "hud-subagent-keep";
4027
+ await initTeamState(teamName, "preserve subagent HUD reconcile", "executor", 1, cwd);
4028
+ await setTeamPaneIds(cwd, teamName, {
4029
+ leaderPaneId: "%42",
4030
+ workerPaneIds: { "worker-1": "%10" },
4031
+ });
4032
+ const stateDir = join(cwd, ".omx", "state");
4033
+ const canonicalSessionId = "sess-subagent-hud-parent";
4034
+ const leaderNativeSessionId = "native-subagent-hud-parent";
4035
+ const childNativeSessionId = "native-subagent-hud-child";
4036
+ const nowIso = new Date().toISOString();
4037
+ await writeJson(join(stateDir, "session.json"), {
4038
+ session_id: canonicalSessionId,
4039
+ native_session_id: leaderNativeSessionId,
4040
+ });
4041
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
4042
+ schemaVersion: 1,
4043
+ sessions: {
4044
+ [canonicalSessionId]: {
4045
+ session_id: canonicalSessionId,
4046
+ leader_thread_id: leaderNativeSessionId,
4047
+ updated_at: nowIso,
4048
+ threads: {
4049
+ [leaderNativeSessionId]: {
4050
+ thread_id: leaderNativeSessionId,
4051
+ kind: "leader",
4052
+ first_seen_at: nowIso,
4053
+ last_seen_at: nowIso,
4054
+ turn_count: 1,
4055
+ },
4056
+ [childNativeSessionId]: {
4057
+ thread_id: childNativeSessionId,
4058
+ kind: "subagent",
4059
+ first_seen_at: nowIso,
4060
+ last_seen_at: nowIso,
4061
+ turn_count: 1,
4062
+ mode: "verifier",
4063
+ },
4064
+ },
4065
+ },
4066
+ },
4067
+ });
4068
+ process.env.TMUX = "1";
4069
+ process.env.TMUX_PANE = "%10";
4070
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
4071
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
4072
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
4073
+
4074
+ let reconcileCall: { cwd: string; sessionId?: string } | null = null;
4075
+ const result = await dispatchCodexNativeHook(
4076
+ {
4077
+ hook_event_name: "UserPromptSubmit",
4078
+ cwd,
4079
+ session_id: childNativeSessionId,
4080
+ thread_id: childNativeSessionId,
4081
+ turn_id: "turn-subagent-hud-child",
4082
+ prompt: "Review the worker patch literally; do not activate $ralplan.",
4083
+ },
4084
+ {
4085
+ cwd,
4086
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
4087
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
4088
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
4089
+ },
4090
+ },
4091
+ );
4092
+
4093
+ assert.equal(result.outputJson, null);
4094
+ assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
4095
+ } finally {
4096
+ await rm(cwd, { recursive: true, force: true });
4097
+ }
4098
+ });
4099
+
3442
4100
  it("runs prompt-submit HUD reconciliation as a best-effort tmux-only side effect", async () => {
3443
4101
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-"));
3444
4102
  const originalTmux = process.env.TMUX;
@@ -3521,6 +4179,78 @@ esac
3521
4179
  }
3522
4180
  });
3523
4181
 
4182
+ it("reuses an existing owner-tagged HUD pane when UserPromptSubmit revives with the canonical session id", async () => {
4183
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-"));
4184
+ const originalTmux = process.env.TMUX;
4185
+ const originalTmuxPane = process.env.TMUX_PANE;
4186
+ const originalPath = process.env.PATH;
4187
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
4188
+ try {
4189
+ process.env.TMUX = "1";
4190
+ process.env.TMUX_PANE = "%1";
4191
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
4192
+ const canonicalSessionId = "omx-canonical-hud-reuse";
4193
+ const nativeSessionId = "codex-native-hud-reuse";
4194
+ await mkdir(join(cwd, ".omx", "state", "sessions", canonicalSessionId), { recursive: true });
4195
+ await writeSessionStart(cwd, canonicalSessionId);
4196
+
4197
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-bin-"));
4198
+ const tmuxLog = join(cwd, "tmux.log");
4199
+ await writeFile(
4200
+ join(binDir, "tmux"),
4201
+ `#!/usr/bin/env bash
4202
+ set -euo pipefail
4203
+ printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
4204
+ case "$1" in
4205
+ list-panes)
4206
+ printf '%%1\tcodex\tcodex\n'
4207
+ printf '%%2\tnode\texec env OMX_TMUX_HUD_OWNER='"'"'1'"'"' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='"'"'%%1'"'"' /node /omx.js hud --watch\n'
4208
+ ;;
4209
+ display-message)
4210
+ printf '80\t24\n'
4211
+ ;;
4212
+ resize-pane)
4213
+ ;;
4214
+ split-window)
4215
+ printf '%%9\n'
4216
+ ;;
4217
+ esac
4218
+ `,
4219
+ );
4220
+ await chmod(join(binDir, "tmux"), 0o755);
4221
+ process.env.PATH = `${binDir}:${originalPath}`;
4222
+
4223
+ const result = await dispatchCodexNativeHook(
4224
+ {
4225
+ hook_event_name: "UserPromptSubmit",
4226
+ cwd,
4227
+ session_id: nativeSessionId,
4228
+ thread_id: "thread-hud-reuse",
4229
+ turn_id: "turn-hud-reuse",
4230
+ prompt: "$ralplan prepare plan",
4231
+ },
4232
+ { cwd },
4233
+ );
4234
+
4235
+ assert.equal(result.omxEventName, "keyword-detector");
4236
+ const tmuxCalls = await readFile(tmuxLog, "utf-8");
4237
+ assert.match(tmuxCalls, /list-panes -t %1 -F/);
4238
+ assert.match(tmuxCalls, /resize-pane -t %2 -y 3/);
4239
+ assert.doesNotMatch(tmuxCalls, /split-window/);
4240
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", canonicalSessionId, "ralplan-state.json")), true);
4241
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", nativeSessionId, "ralplan-state.json")), false);
4242
+ } finally {
4243
+ if (originalTmux === undefined) delete process.env.TMUX;
4244
+ else process.env.TMUX = originalTmux;
4245
+ if (originalTmuxPane === undefined) delete process.env.TMUX_PANE;
4246
+ else process.env.TMUX_PANE = originalTmuxPane;
4247
+ if (originalHudOwner === undefined) delete process.env[OMX_TMUX_HUD_OWNER_ENV];
4248
+ else process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
4249
+ process.env.PATH = originalPath;
4250
+ await rm(cwd, { recursive: true, force: true });
4251
+ }
4252
+ });
4253
+
3524
4254
  it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
3525
4255
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
3526
4256
  const originalTmux = process.env.TMUX;
@@ -8779,6 +9509,70 @@ exit 0
8779
9509
  }
8780
9510
  });
8781
9511
 
9512
+ it("does not report ralplan subagent waiting when notify-fallback already recorded completion", async () => {
9513
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-complete-"));
9514
+ try {
9515
+ const stateDir = join(cwd, ".omx", "state");
9516
+ const now = new Date().toISOString();
9517
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent-complete"), { recursive: true });
9518
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent-complete" });
9519
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent-complete", "skill-active-state.json"), {
9520
+ active: true,
9521
+ skill: "ralplan",
9522
+ phase: "planning",
9523
+ });
9524
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent-complete", "ralplan-state.json"), {
9525
+ active: true,
9526
+ current_phase: "planning",
9527
+ });
9528
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
9529
+ schemaVersion: 1,
9530
+ sessions: {
9531
+ "sess-stop-skill-subagent-complete": {
9532
+ session_id: "sess-stop-skill-subagent-complete",
9533
+ leader_thread_id: "leader-1",
9534
+ updated_at: now,
9535
+ threads: {
9536
+ "leader-1": {
9537
+ thread_id: "leader-1",
9538
+ kind: "leader",
9539
+ first_seen_at: now,
9540
+ last_seen_at: now,
9541
+ turn_count: 1,
9542
+ },
9543
+ "sub-1": {
9544
+ thread_id: "sub-1",
9545
+ kind: "subagent",
9546
+ first_seen_at: now,
9547
+ last_seen_at: now,
9548
+ completed_at: now,
9549
+ last_completed_turn_id: "turn-complete-1",
9550
+ completion_source: "notify-fallback-watcher",
9551
+ turn_count: 2,
9552
+ },
9553
+ },
9554
+ },
9555
+ },
9556
+ });
9557
+
9558
+ const result = await dispatchCodexNativeHook(
9559
+ {
9560
+ hook_event_name: "Stop",
9561
+ cwd,
9562
+ session_id: "sess-stop-skill-subagent-complete",
9563
+ },
9564
+ { cwd },
9565
+ );
9566
+
9567
+ assert.equal(result.omxEventName, "stop");
9568
+ assert.equal(result.outputJson?.decision, "block");
9569
+ assert.doesNotMatch(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
9570
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
9571
+ } finally {
9572
+ await rm(cwd, { recursive: true, force: true });
9573
+ }
9574
+ });
9575
+
8782
9576
  it("does not block on stale root ralplan skill when the explicit session-scoped canonical skill state is absent", async () => {
8783
9577
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-"));
8784
9578
  try {
@@ -13348,3 +14142,62 @@ describe("codex native hook triage integration", () => {
13348
14142
  }
13349
14143
  });
13350
14144
  });
14145
+
14146
+ describe('native Stop autopilot deep-interview wait', () => {
14147
+ it('does not force continued execution while autopilot is waiting on a deep-interview omx question', async () => {
14148
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-native-hook-autopilot-question-wait-'));
14149
+ try {
14150
+ const sessionId = 'sess-autopilot-wait';
14151
+ const sessionDir = join(cwd, '.omx', 'state', 'sessions', sessionId);
14152
+ await writeJson(join(cwd, '.omx', 'state', 'session.json'), { session_id: sessionId });
14153
+ await writeJson(join(sessionDir, 'autopilot-state.json'), {
14154
+ mode: 'autopilot',
14155
+ active: true,
14156
+ current_phase: 'waiting-for-user',
14157
+ run_outcome: 'blocked_on_user',
14158
+ lifecycle_outcome: 'askuserQuestion',
14159
+ session_id: sessionId,
14160
+ state: {
14161
+ deep_interview_question: {
14162
+ status: 'waiting_for_user',
14163
+ source: 'omx-question',
14164
+ obligation_id: 'obligation-stop-1',
14165
+ previous_phase: 'deep-interview',
14166
+ },
14167
+ },
14168
+ });
14169
+ await writeJson(join(sessionDir, 'deep-interview-state.json'), {
14170
+ mode: 'deep-interview',
14171
+ active: false,
14172
+ current_phase: 'intent-first',
14173
+ lifecycle_outcome: 'askuserQuestion',
14174
+ run_outcome: 'blocked_on_user',
14175
+ session_id: sessionId,
14176
+ question_enforcement: {
14177
+ obligation_id: 'obligation-stop-1',
14178
+ source: 'omx-question',
14179
+ status: 'pending',
14180
+ lifecycle_outcome: 'askuserQuestion',
14181
+ requested_at: '2026-04-19T00:00:00.000Z',
14182
+ },
14183
+ });
14184
+ await writeJson(join(sessionDir, 'skill-active-state.json'), {
14185
+ active: true,
14186
+ skill: 'autopilot',
14187
+ phase: 'deep-interview',
14188
+ session_id: sessionId,
14189
+ active_skills: [{ skill: 'autopilot', phase: 'deep-interview', active: true, session_id: sessionId }],
14190
+ });
14191
+
14192
+ const result = await dispatchCodexNativeHook({
14193
+ hook_event_name: 'Stop',
14194
+ session_id: sessionId,
14195
+ thread_id: 'thread-autopilot-wait',
14196
+ }, { cwd });
14197
+
14198
+ assert.equal(result.outputJson, null);
14199
+ } finally {
14200
+ await rm(cwd, { recursive: true, force: true });
14201
+ }
14202
+ });
14203
+ });