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
@@ -14,6 +14,7 @@ import { writeSessionStart } from "../../hooks/session.js";
14
14
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
15
15
  import { executeStateOperation } from "../../state/operations.js";
16
16
  import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
17
+ import { OMX_TMUX_HUD_LEADER_PANE_ENV } from "../../hud/tmux.js";
17
18
  import { readAllState } from "../../hud/state.js";
18
19
  import { renderHud } from "../../hud/render.js";
19
20
  import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
@@ -42,6 +43,33 @@ async function writeJson(path, value) {
42
43
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
43
44
  await writeFile(path, JSON.stringify(value, null, 2));
44
45
  }
46
+ async function setTeamPaneIds(cwd, teamName, paneIds) {
47
+ for (const fileName of ["config.json", "manifest.v2.json"]) {
48
+ const filePath = join(cwd, ".omx", "state", "team", teamName, fileName);
49
+ const parsed = JSON.parse(await readFile(filePath, "utf-8"));
50
+ parsed.leader_pane_id = paneIds.leaderPaneId;
51
+ parsed.workers = (parsed.workers ?? []).map((worker) => ({
52
+ ...worker,
53
+ pane_id: worker.name ? paneIds.workerPaneIds[worker.name] ?? worker.pane_id ?? null : worker.pane_id ?? null,
54
+ }));
55
+ await writeJson(filePath, parsed);
56
+ }
57
+ }
58
+ async function withIsolatedHome(prefix, run) {
59
+ const homeDir = await mkdtemp(join(tmpdir(), `omx-native-hook-home-${prefix}-`));
60
+ const previousHome = process.env.HOME;
61
+ try {
62
+ process.env.HOME = homeDir;
63
+ return await run(homeDir);
64
+ }
65
+ finally {
66
+ if (typeof previousHome === "string")
67
+ process.env.HOME = previousHome;
68
+ else
69
+ delete process.env.HOME;
70
+ await rm(homeDir, { recursive: true, force: true });
71
+ }
72
+ }
45
73
  async function withLoreGuardConfig(value, prefix, run) {
46
74
  const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-pretool-git-commit-lore-${prefix}-`));
47
75
  const codexHome = await mkdtemp(join(tmpdir(), `omx-native-hook-codex-home-lore-${prefix}-`));
@@ -176,6 +204,7 @@ const TEAM_STOP_COMMIT_GUIDANCE = " If system-generated worker auto-checkpoint c
176
204
  const DEFAULT_AUTO_NUDGE_RESPONSE = "continue with the current task only if it is already authorized";
177
205
  const TEAM_ENV_KEYS = [
178
206
  "OMX_TEAM_WORKER",
207
+ "OMX_TEAM_INTERNAL_WORKER",
179
208
  "OMX_TEAM_STATE_ROOT",
180
209
  "OMX_TEAM_LEADER_CWD",
181
210
  "OMX_SESSION_ID",
@@ -1291,6 +1320,308 @@ describe("codex native hook dispatch", () => {
1291
1320
  await rm(cwd, { recursive: true, force: true });
1292
1321
  }
1293
1322
  });
1323
+ it("injects deep-interview config overrides into UserPromptSubmit developer context", async () => {
1324
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-"));
1325
+ try {
1326
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1327
+ await writeFile(join(cwd, ".omx", "config.toml"), `[omx.deepInterview]
1328
+ defaultProfile = "standard"
1329
+ standardThreshold = 0.05
1330
+ standardMaxRounds = 15
1331
+ enableChallengeModes = false
1332
+ `);
1333
+ const result = await dispatchCodexNativeHook({
1334
+ hook_event_name: "UserPromptSubmit",
1335
+ cwd,
1336
+ session_id: "sess-deep-interview-config",
1337
+ thread_id: "thread-1",
1338
+ turn_id: "turn-1",
1339
+ prompt: "$deep-interview prove config reflection",
1340
+ }, { cwd });
1341
+ assert.equal(result.omxEventName, "keyword-detector");
1342
+ assert.equal(result.skillState?.skill, "deep-interview");
1343
+ const serializedOutput = JSON.stringify(result.outputJson);
1344
+ assert.match(serializedOutput, /Deep-interview config override active/);
1345
+ assert.match(serializedOutput, /threshold=0\.05/);
1346
+ assert.match(serializedOutput, /max_rounds=15/);
1347
+ assert.match(serializedOutput, /enableChallengeModes=false/);
1348
+ const modeState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", "sess-deep-interview-config", "deep-interview-state.json"), "utf-8"));
1349
+ assert.equal(modeState.profile, "standard");
1350
+ assert.equal(modeState.threshold, 0.05);
1351
+ assert.equal(modeState.max_rounds, 15);
1352
+ }
1353
+ finally {
1354
+ await rm(cwd, { recursive: true, force: true });
1355
+ }
1356
+ });
1357
+ it("proves UserPromptSubmit context changes before and after adding deep-interview config", async () => {
1358
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-before-after-"));
1359
+ const sessionId = "sess-deep-interview-config-before-after";
1360
+ try {
1361
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1362
+ const before = await withIsolatedHome("deep-interview-config-before-after", async () => (dispatchCodexNativeHook({
1363
+ hook_event_name: "UserPromptSubmit",
1364
+ cwd,
1365
+ session_id: sessionId,
1366
+ thread_id: "thread-before-after",
1367
+ turn_id: "turn-before",
1368
+ prompt: "$deep-interview prove before config context",
1369
+ }, { cwd })));
1370
+ const beforeOutput = JSON.stringify(before.outputJson);
1371
+ const beforeState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"));
1372
+ assert.equal(before.skillState?.skill, "deep-interview");
1373
+ assert.doesNotMatch(beforeOutput, /Deep-interview config override active/);
1374
+ assert.equal(before.skillState?.deep_interview_config, undefined);
1375
+ assert.equal(beforeState.deep_interview_config, undefined);
1376
+ assert.equal(beforeState.threshold, undefined);
1377
+ assert.equal(beforeState.max_rounds, undefined);
1378
+ await writeFile(join(cwd, ".omx", "config.toml"), `[omx.deepInterview]
1379
+ defaultProfile = "standard"
1380
+ standardThreshold = 0.05
1381
+ standardMaxRounds = 15
1382
+ `);
1383
+ const after = await dispatchCodexNativeHook({
1384
+ hook_event_name: "UserPromptSubmit",
1385
+ cwd,
1386
+ session_id: sessionId,
1387
+ thread_id: "thread-before-after",
1388
+ turn_id: "turn-after",
1389
+ prompt: "$deep-interview prove after config context",
1390
+ }, { cwd });
1391
+ const afterOutput = JSON.stringify(after.outputJson);
1392
+ const afterState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"));
1393
+ assert.equal(after.skillState?.deep_interview_config?.profile, "standard");
1394
+ assert.match(afterOutput, /Deep-interview config override active/);
1395
+ assert.match(afterOutput, /threshold=0\.05/);
1396
+ assert.match(afterOutput, /max_rounds=15/);
1397
+ assert.equal(afterState.deep_interview_config?.profile, "standard");
1398
+ assert.equal(afterState.threshold, 0.05);
1399
+ assert.equal(afterState.max_rounds, 15);
1400
+ }
1401
+ finally {
1402
+ await rm(cwd, { recursive: true, force: true });
1403
+ }
1404
+ });
1405
+ it("injects deep-interview config for mixed workflow prompts that defer execution modes", async () => {
1406
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-mixed-"));
1407
+ const sessionId = "sess-deep-interview-config-mixed";
1408
+ try {
1409
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1410
+ await writeFile(join(cwd, ".omx", "config.toml"), `[omx.deepInterview]
1411
+ defaultProfile = "deep"
1412
+ deepThreshold = 0.13
1413
+ deepMaxRounds = 21
1414
+ enableChallengeModes = false
1415
+ `);
1416
+ const result = await withIsolatedHome("deep-interview-config-mixed", async () => (dispatchCodexNativeHook({
1417
+ hook_event_name: "UserPromptSubmit",
1418
+ cwd,
1419
+ session_id: sessionId,
1420
+ thread_id: "thread-mixed-config",
1421
+ turn_id: "turn-mixed-config",
1422
+ prompt: "$autopilot $deep-interview prove mixed config context",
1423
+ }, { cwd })));
1424
+ const serializedOutput = JSON.stringify(result.outputJson);
1425
+ const modeState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"));
1426
+ assert.equal(result.skillState?.skill, "deep-interview");
1427
+ assert.deepEqual(result.skillState?.deferred_skills, ["autopilot"]);
1428
+ assert.equal(result.skillState?.deep_interview_config?.profile, "deep");
1429
+ assert.equal(result.skillState?.deep_interview_config?.threshold, 0.13);
1430
+ assert.equal(result.skillState?.deep_interview_config?.maxRounds, 21);
1431
+ assert.equal(result.skillState?.deep_interview_config?.enableChallengeModes, false);
1432
+ assert.match(serializedOutput, /Deep-interview config override active/);
1433
+ assert.match(serializedOutput, /profile=deep/);
1434
+ assert.match(serializedOutput, /threshold=0\.13/);
1435
+ assert.match(serializedOutput, /max_rounds=21/);
1436
+ assert.match(serializedOutput, /enableChallengeModes=false/);
1437
+ assert.equal(modeState.deep_interview_config?.profile, "deep");
1438
+ assert.equal(modeState.profile, "deep");
1439
+ assert.equal(modeState.threshold, 0.13);
1440
+ assert.equal(modeState.max_rounds, 21);
1441
+ assert.equal(modeState.enable_challenge_modes, false);
1442
+ }
1443
+ finally {
1444
+ await rm(cwd, { recursive: true, force: true });
1445
+ }
1446
+ });
1447
+ it("keeps deep-interview config override context on continuation prompts", async () => {
1448
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-continuation-"));
1449
+ const sessionId = "sess-deep-interview-config-continuation";
1450
+ try {
1451
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1452
+ await writeFile(join(cwd, ".omx", "config.toml"), `[omx.deepInterview]
1453
+ defaultProfile = "standard"
1454
+ standardThreshold = 0.05
1455
+ standardMaxRounds = 15
1456
+ `);
1457
+ await dispatchCodexNativeHook({
1458
+ hook_event_name: "UserPromptSubmit",
1459
+ cwd,
1460
+ session_id: sessionId,
1461
+ thread_id: "thread-continuation",
1462
+ turn_id: "turn-start",
1463
+ prompt: "$deep-interview prove config continuation",
1464
+ }, { cwd });
1465
+ const continued = await dispatchCodexNativeHook({
1466
+ hook_event_name: "UserPromptSubmit",
1467
+ cwd,
1468
+ session_id: sessionId,
1469
+ thread_id: "thread-continuation",
1470
+ turn_id: "turn-continue",
1471
+ prompt: "continue",
1472
+ }, { cwd });
1473
+ const serializedOutput = JSON.stringify(continued.outputJson);
1474
+ const modeState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"));
1475
+ assert.equal(continued.skillState?.skill, "deep-interview");
1476
+ assert.match(serializedOutput, /Deep-interview config override active/);
1477
+ assert.match(serializedOutput, /threshold=0\.05/);
1478
+ assert.match(serializedOutput, /max_rounds=15/);
1479
+ assert.equal(modeState.profile, "standard");
1480
+ assert.equal(modeState.threshold, 0.05);
1481
+ assert.equal(modeState.max_rounds, 15);
1482
+ }
1483
+ finally {
1484
+ await rm(cwd, { recursive: true, force: true });
1485
+ }
1486
+ });
1487
+ it("keeps explicit deep-interview profile flags reflected on continuation prompts", async () => {
1488
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-profile-continuation-"));
1489
+ const sessionId = "sess-deep-interview-config-profile-continuation";
1490
+ try {
1491
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1492
+ await writeFile(join(cwd, ".omx", "config.toml"), `[omx.deepInterview]
1493
+ defaultProfile = "standard"
1494
+ standardThreshold = 0.22
1495
+ standardMaxRounds = 13
1496
+ deepThreshold = 0.13
1497
+ deepMaxRounds = 21
1498
+ `);
1499
+ await dispatchCodexNativeHook({
1500
+ hook_event_name: "UserPromptSubmit",
1501
+ cwd,
1502
+ session_id: sessionId,
1503
+ thread_id: "thread-profile-continuation",
1504
+ turn_id: "turn-start",
1505
+ prompt: "$deep-interview --deep prove explicit profile continuation",
1506
+ }, { cwd });
1507
+ const continued = await dispatchCodexNativeHook({
1508
+ hook_event_name: "UserPromptSubmit",
1509
+ cwd,
1510
+ session_id: sessionId,
1511
+ thread_id: "thread-profile-continuation",
1512
+ turn_id: "turn-continue",
1513
+ prompt: "continue",
1514
+ }, { cwd });
1515
+ const serializedOutput = JSON.stringify(continued.outputJson);
1516
+ const modeState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"));
1517
+ assert.equal(continued.skillState?.skill, "deep-interview");
1518
+ assert.equal(continued.skillState?.deep_interview_config?.profile, "deep");
1519
+ assert.match(serializedOutput, /Deep-interview config override active/);
1520
+ assert.match(serializedOutput, /profile=deep/);
1521
+ assert.match(serializedOutput, /threshold=0\.13/);
1522
+ assert.match(serializedOutput, /max_rounds=21/);
1523
+ assert.equal(modeState.deep_interview_config?.profile, "deep");
1524
+ assert.equal(modeState.profile, "deep");
1525
+ assert.equal(modeState.threshold, 0.13);
1526
+ assert.equal(modeState.max_rounds, 21);
1527
+ }
1528
+ finally {
1529
+ await rm(cwd, { recursive: true, force: true });
1530
+ }
1531
+ });
1532
+ it("keeps the documented deep-interview Suggested Config reflected in UserPromptSubmit context", async () => {
1533
+ const skillDoc = await readFile(join(process.cwd(), "skills", "deep-interview", "SKILL.md"), "utf-8");
1534
+ const markerIndex = skillDoc.indexOf("## Suggested Config (optional)");
1535
+ assert.notEqual(markerIndex, -1);
1536
+ const configMatch = skillDoc.slice(markerIndex).match(/```toml\n([\s\S]*?)\n```/);
1537
+ assert.ok(configMatch);
1538
+ const documentedConfig = configMatch[1]?.trimEnd();
1539
+ assert.ok(documentedConfig);
1540
+ assert.match(documentedConfig, /standardThreshold = 0\.20/);
1541
+ assert.match(documentedConfig, /standardMaxRounds = 12/);
1542
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-doc-config-"));
1543
+ const sessionId = "sess-deep-interview-doc-config";
1544
+ try {
1545
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1546
+ await writeFile(join(cwd, ".omx", "config.toml"), `${documentedConfig}\n`);
1547
+ const result = await dispatchCodexNativeHook({
1548
+ hook_event_name: "UserPromptSubmit",
1549
+ cwd,
1550
+ session_id: sessionId,
1551
+ thread_id: "thread-doc-config",
1552
+ turn_id: "turn-doc-config",
1553
+ prompt: "$deep-interview prove documented config context",
1554
+ }, { cwd });
1555
+ const serializedOutput = JSON.stringify(result.outputJson);
1556
+ const modeState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"));
1557
+ assert.equal(result.skillState?.deep_interview_config?.profile, "standard");
1558
+ assert.equal(result.skillState?.deep_interview_config?.threshold, 0.2);
1559
+ assert.equal(result.skillState?.deep_interview_config?.maxRounds, 12);
1560
+ assert.match(serializedOutput, /Deep-interview config override active/);
1561
+ assert.match(serializedOutput, /profile=standard/);
1562
+ assert.match(serializedOutput, /threshold=0\.2/);
1563
+ assert.match(serializedOutput, /max_rounds=12/);
1564
+ assert.equal(modeState.deep_interview_config?.profile, "standard");
1565
+ assert.equal(modeState.profile, "standard");
1566
+ assert.equal(modeState.threshold, 0.2);
1567
+ assert.equal(modeState.max_rounds, 12);
1568
+ }
1569
+ finally {
1570
+ await rm(cwd, { recursive: true, force: true });
1571
+ }
1572
+ });
1573
+ it("injects deep-interview config overrides when state is boxed under OMX_ROOT", async () => {
1574
+ const root = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-boxed-"));
1575
+ const cwd = join(root, "source");
1576
+ const omxRoot = join(root, "box");
1577
+ const sessionId = "sess-boxed-deep-interview-config";
1578
+ const previousOmxRoot = process.env.OMX_ROOT;
1579
+ const previousOmxStateRoot = process.env.OMX_STATE_ROOT;
1580
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1581
+ try {
1582
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1583
+ await writeFile(join(cwd, ".omx", "config.toml"), `[omx.deepInterview]
1584
+ defaultProfile = "standard"
1585
+ standardThreshold = 0.05
1586
+ standardMaxRounds = 15
1587
+ `);
1588
+ process.env.OMX_ROOT = omxRoot;
1589
+ delete process.env.OMX_STATE_ROOT;
1590
+ delete process.env.OMX_TEAM_STATE_ROOT;
1591
+ const result = await dispatchCodexNativeHook({
1592
+ hook_event_name: "UserPromptSubmit",
1593
+ cwd,
1594
+ session_id: sessionId,
1595
+ thread_id: "thread-boxed",
1596
+ turn_id: "turn-boxed",
1597
+ prompt: "$deep-interview prove boxed config reflection",
1598
+ }, { cwd });
1599
+ assert.equal(result.omxEventName, "keyword-detector");
1600
+ assert.equal(result.skillState?.initialized_state_path, `.omx/state/sessions/${sessionId}/deep-interview-state.json`);
1601
+ const boxedStatePath = join(omxRoot, ".omx", "state", "sessions", sessionId, "deep-interview-state.json");
1602
+ assert.equal(existsSync(boxedStatePath), true);
1603
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json")), false);
1604
+ const serializedOutput = JSON.stringify(result.outputJson);
1605
+ assert.match(serializedOutput, /Deep-interview config override active/);
1606
+ assert.match(serializedOutput, /threshold=0\.05/);
1607
+ assert.match(serializedOutput, /max_rounds=15/);
1608
+ }
1609
+ finally {
1610
+ if (typeof previousOmxRoot === "string")
1611
+ process.env.OMX_ROOT = previousOmxRoot;
1612
+ else
1613
+ delete process.env.OMX_ROOT;
1614
+ if (typeof previousOmxStateRoot === "string")
1615
+ process.env.OMX_STATE_ROOT = previousOmxStateRoot;
1616
+ else
1617
+ delete process.env.OMX_STATE_ROOT;
1618
+ if (typeof previousTeamStateRoot === "string")
1619
+ process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
1620
+ else
1621
+ delete process.env.OMX_TEAM_STATE_ROOT;
1622
+ await rm(root, { recursive: true, force: true });
1623
+ }
1624
+ });
1294
1625
  it("records boxed keyword activation mode detail and skill state under OMX_ROOT", async () => {
1295
1626
  const root = await mkdtemp(join(tmpdir(), "omx-native-hook-boxed-"));
1296
1627
  const cwd = join(root, "source");
@@ -1605,6 +1936,36 @@ describe("codex native hook dispatch", () => {
1605
1936
  await rm(cwd, { recursive: true, force: true });
1606
1937
  }
1607
1938
  });
1939
+ it("does not repeat ultragoal Stop recovery after a safe completed-aggregate microgoal blocker is recorded", async () => {
1940
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-aggregate-blocked-stop-"));
1941
+ try {
1942
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
1943
+ version: 1,
1944
+ codexGoalMode: "aggregate",
1945
+ activeGoalId: "G001-demo",
1946
+ goals: [{
1947
+ id: "G001-demo",
1948
+ status: "in_progress",
1949
+ objective: "Demo goal",
1950
+ 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",
1951
+ }],
1952
+ });
1953
+ const result = await dispatchCodexNativeHook({
1954
+ hook_event_name: "Stop",
1955
+ cwd,
1956
+ session_id: "sess-ultragoal-aggregate-blocked-stop",
1957
+ thread_id: "thread-ultragoal-aggregate-blocked-stop",
1958
+ stop_hook_active: true,
1959
+ last_assistant_message: "Goal complete.",
1960
+ }, { cwd });
1961
+ assert.notEqual(result.outputJson?.decision, "block");
1962
+ assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
1963
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
1964
+ }
1965
+ finally {
1966
+ await rm(cwd, { recursive: true, force: true });
1967
+ }
1968
+ });
1608
1969
  it("does not block ultragoal Stop after task-scoped reconciliation finishes exploded bookkeeping", async () => {
1609
1970
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
1610
1971
  try {
@@ -2852,6 +3213,178 @@ export async function onHookEvent(event) {
2852
3213
  await rm(cwd, { recursive: true, force: true });
2853
3214
  }
2854
3215
  });
3216
+ it("skips prompt-submit HUD reconciliation for confirmed team worker panes", async () => {
3217
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-worker-skip-"));
3218
+ try {
3219
+ const teamName = "hud-worker-skip";
3220
+ await initTeamState(teamName, "skip worker HUD reconcile", "executor", 1, cwd);
3221
+ await setTeamPaneIds(cwd, teamName, {
3222
+ leaderPaneId: "%42",
3223
+ workerPaneIds: { "worker-1": "%10" },
3224
+ });
3225
+ process.env.TMUX = "1";
3226
+ process.env.TMUX_PANE = "%10";
3227
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
3228
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
3229
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3230
+ let reconcileCalls = 0;
3231
+ const result = await dispatchCodexNativeHook({
3232
+ hook_event_name: "UserPromptSubmit",
3233
+ cwd,
3234
+ session_id: "sess-hud-team-worker",
3235
+ prompt: "$ralplan prepare plan",
3236
+ }, {
3237
+ cwd,
3238
+ reconcileHudForPromptSubmitFn: async () => {
3239
+ reconcileCalls += 1;
3240
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3241
+ },
3242
+ });
3243
+ assert.equal(result.omxEventName, "keyword-detector");
3244
+ assert.equal(reconcileCalls, 0);
3245
+ }
3246
+ finally {
3247
+ await rm(cwd, { recursive: true, force: true });
3248
+ }
3249
+ });
3250
+ it("preserves prompt-submit HUD reconciliation for team leader panes", async () => {
3251
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-leader-preserve-"));
3252
+ try {
3253
+ const teamName = "hud-leader-keep";
3254
+ await initTeamState(teamName, "preserve leader HUD reconcile", "executor", 1, cwd);
3255
+ await setTeamPaneIds(cwd, teamName, {
3256
+ leaderPaneId: "%42",
3257
+ workerPaneIds: { "worker-1": "%10" },
3258
+ });
3259
+ process.env.TMUX = "1";
3260
+ process.env.TMUX_PANE = "%42";
3261
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3262
+ let reconcileCall = null;
3263
+ const result = await dispatchCodexNativeHook({
3264
+ hook_event_name: "UserPromptSubmit",
3265
+ cwd,
3266
+ session_id: "sess-hud-team-leader",
3267
+ prompt: "$ralplan prepare plan",
3268
+ }, {
3269
+ cwd,
3270
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
3271
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
3272
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3273
+ },
3274
+ });
3275
+ assert.equal(result.omxEventName, "keyword-detector");
3276
+ assert.deepEqual(reconcileCall, { cwd, sessionId: "sess-hud-team-leader" });
3277
+ }
3278
+ finally {
3279
+ await rm(cwd, { recursive: true, force: true });
3280
+ }
3281
+ });
3282
+ it("preserves prompt-submit HUD reconciliation when worker pane detection is ambiguous", async () => {
3283
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-worker-ambiguous-"));
3284
+ try {
3285
+ const teamName = "hud-worker-ambiguous";
3286
+ await initTeamState(teamName, "fail closed for ambiguous worker HUD reconcile", "executor", 1, cwd);
3287
+ await setTeamPaneIds(cwd, teamName, {
3288
+ leaderPaneId: "%42",
3289
+ workerPaneIds: { "worker-1": "%10" },
3290
+ });
3291
+ process.env.TMUX = "1";
3292
+ process.env.TMUX_PANE = "%99";
3293
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
3294
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
3295
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3296
+ let reconcileCalls = 0;
3297
+ const result = await dispatchCodexNativeHook({
3298
+ hook_event_name: "UserPromptSubmit",
3299
+ cwd,
3300
+ session_id: "sess-hud-team-worker-ambiguous",
3301
+ prompt: "$ralplan prepare plan",
3302
+ }, {
3303
+ cwd,
3304
+ reconcileHudForPromptSubmitFn: async () => {
3305
+ reconcileCalls += 1;
3306
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3307
+ },
3308
+ });
3309
+ assert.equal(result.omxEventName, "keyword-detector");
3310
+ assert.equal(reconcileCalls, 1);
3311
+ }
3312
+ finally {
3313
+ await rm(cwd, { recursive: true, force: true });
3314
+ }
3315
+ });
3316
+ it("preserves prompt-submit HUD reconciliation for native subagents even with worker pane env", async () => {
3317
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-subagent-worker-preserve-"));
3318
+ try {
3319
+ const teamName = "hud-subagent-keep";
3320
+ await initTeamState(teamName, "preserve subagent HUD reconcile", "executor", 1, cwd);
3321
+ await setTeamPaneIds(cwd, teamName, {
3322
+ leaderPaneId: "%42",
3323
+ workerPaneIds: { "worker-1": "%10" },
3324
+ });
3325
+ const stateDir = join(cwd, ".omx", "state");
3326
+ const canonicalSessionId = "sess-subagent-hud-parent";
3327
+ const leaderNativeSessionId = "native-subagent-hud-parent";
3328
+ const childNativeSessionId = "native-subagent-hud-child";
3329
+ const nowIso = new Date().toISOString();
3330
+ await writeJson(join(stateDir, "session.json"), {
3331
+ session_id: canonicalSessionId,
3332
+ native_session_id: leaderNativeSessionId,
3333
+ });
3334
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
3335
+ schemaVersion: 1,
3336
+ sessions: {
3337
+ [canonicalSessionId]: {
3338
+ session_id: canonicalSessionId,
3339
+ leader_thread_id: leaderNativeSessionId,
3340
+ updated_at: nowIso,
3341
+ threads: {
3342
+ [leaderNativeSessionId]: {
3343
+ thread_id: leaderNativeSessionId,
3344
+ kind: "leader",
3345
+ first_seen_at: nowIso,
3346
+ last_seen_at: nowIso,
3347
+ turn_count: 1,
3348
+ },
3349
+ [childNativeSessionId]: {
3350
+ thread_id: childNativeSessionId,
3351
+ kind: "subagent",
3352
+ first_seen_at: nowIso,
3353
+ last_seen_at: nowIso,
3354
+ turn_count: 1,
3355
+ mode: "verifier",
3356
+ },
3357
+ },
3358
+ },
3359
+ },
3360
+ });
3361
+ process.env.TMUX = "1";
3362
+ process.env.TMUX_PANE = "%10";
3363
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
3364
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
3365
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3366
+ let reconcileCall = null;
3367
+ const result = await dispatchCodexNativeHook({
3368
+ hook_event_name: "UserPromptSubmit",
3369
+ cwd,
3370
+ session_id: childNativeSessionId,
3371
+ thread_id: childNativeSessionId,
3372
+ turn_id: "turn-subagent-hud-child",
3373
+ prompt: "Review the worker patch literally; do not activate $ralplan.",
3374
+ }, {
3375
+ cwd,
3376
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
3377
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
3378
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3379
+ },
3380
+ });
3381
+ assert.equal(result.outputJson, null);
3382
+ assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
3383
+ }
3384
+ finally {
3385
+ await rm(cwd, { recursive: true, force: true });
3386
+ }
3387
+ });
2855
3388
  it("runs prompt-submit HUD reconciliation as a best-effort tmux-only side effect", async () => {
2856
3389
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-"));
2857
3390
  const originalTmux = process.env.TMUX;
@@ -2925,6 +3458,75 @@ esac
2925
3458
  await rm(cwd, { recursive: true, force: true });
2926
3459
  }
2927
3460
  });
3461
+ it("reuses an existing owner-tagged HUD pane when UserPromptSubmit revives with the canonical session id", async () => {
3462
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-"));
3463
+ const originalTmux = process.env.TMUX;
3464
+ const originalTmuxPane = process.env.TMUX_PANE;
3465
+ const originalPath = process.env.PATH;
3466
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
3467
+ try {
3468
+ process.env.TMUX = "1";
3469
+ process.env.TMUX_PANE = "%1";
3470
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3471
+ const canonicalSessionId = "omx-canonical-hud-reuse";
3472
+ const nativeSessionId = "codex-native-hud-reuse";
3473
+ await mkdir(join(cwd, ".omx", "state", "sessions", canonicalSessionId), { recursive: true });
3474
+ await writeSessionStart(cwd, canonicalSessionId);
3475
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-bin-"));
3476
+ const tmuxLog = join(cwd, "tmux.log");
3477
+ await writeFile(join(binDir, "tmux"), `#!/usr/bin/env bash
3478
+ set -euo pipefail
3479
+ printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
3480
+ case "$1" in
3481
+ list-panes)
3482
+ printf '%%1\tcodex\tcodex\n'
3483
+ printf '%%2\tnode\texec env OMX_TMUX_HUD_OWNER='"'"'1'"'"' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='"'"'%%1'"'"' /node /omx.js hud --watch\n'
3484
+ ;;
3485
+ display-message)
3486
+ printf '80\t24\n'
3487
+ ;;
3488
+ resize-pane)
3489
+ ;;
3490
+ split-window)
3491
+ printf '%%9\n'
3492
+ ;;
3493
+ esac
3494
+ `);
3495
+ await chmod(join(binDir, "tmux"), 0o755);
3496
+ process.env.PATH = `${binDir}:${originalPath}`;
3497
+ const result = await dispatchCodexNativeHook({
3498
+ hook_event_name: "UserPromptSubmit",
3499
+ cwd,
3500
+ session_id: nativeSessionId,
3501
+ thread_id: "thread-hud-reuse",
3502
+ turn_id: "turn-hud-reuse",
3503
+ prompt: "$ralplan prepare plan",
3504
+ }, { cwd });
3505
+ assert.equal(result.omxEventName, "keyword-detector");
3506
+ const tmuxCalls = await readFile(tmuxLog, "utf-8");
3507
+ assert.match(tmuxCalls, /list-panes -t %1 -F/);
3508
+ assert.match(tmuxCalls, /resize-pane -t %2 -y 3/);
3509
+ assert.doesNotMatch(tmuxCalls, /split-window/);
3510
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", canonicalSessionId, "ralplan-state.json")), true);
3511
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", nativeSessionId, "ralplan-state.json")), false);
3512
+ }
3513
+ finally {
3514
+ if (originalTmux === undefined)
3515
+ delete process.env.TMUX;
3516
+ else
3517
+ process.env.TMUX = originalTmux;
3518
+ if (originalTmuxPane === undefined)
3519
+ delete process.env.TMUX_PANE;
3520
+ else
3521
+ process.env.TMUX_PANE = originalTmuxPane;
3522
+ if (originalHudOwner === undefined)
3523
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
3524
+ else
3525
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
3526
+ process.env.PATH = originalPath;
3527
+ await rm(cwd, { recursive: true, force: true });
3528
+ }
3529
+ });
2928
3530
  it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
2929
3531
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
2930
3532
  const originalTmux = process.env.TMUX;
@@ -7263,6 +7865,65 @@ exit 0
7263
7865
  await rm(cwd, { recursive: true, force: true });
7264
7866
  }
7265
7867
  });
7868
+ it("does not report ralplan subagent waiting when notify-fallback already recorded completion", async () => {
7869
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-complete-"));
7870
+ try {
7871
+ const stateDir = join(cwd, ".omx", "state");
7872
+ const now = new Date().toISOString();
7873
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent-complete"), { recursive: true });
7874
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent-complete" });
7875
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent-complete", "skill-active-state.json"), {
7876
+ active: true,
7877
+ skill: "ralplan",
7878
+ phase: "planning",
7879
+ });
7880
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent-complete", "ralplan-state.json"), {
7881
+ active: true,
7882
+ current_phase: "planning",
7883
+ });
7884
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
7885
+ schemaVersion: 1,
7886
+ sessions: {
7887
+ "sess-stop-skill-subagent-complete": {
7888
+ session_id: "sess-stop-skill-subagent-complete",
7889
+ leader_thread_id: "leader-1",
7890
+ updated_at: now,
7891
+ threads: {
7892
+ "leader-1": {
7893
+ thread_id: "leader-1",
7894
+ kind: "leader",
7895
+ first_seen_at: now,
7896
+ last_seen_at: now,
7897
+ turn_count: 1,
7898
+ },
7899
+ "sub-1": {
7900
+ thread_id: "sub-1",
7901
+ kind: "subagent",
7902
+ first_seen_at: now,
7903
+ last_seen_at: now,
7904
+ completed_at: now,
7905
+ last_completed_turn_id: "turn-complete-1",
7906
+ completion_source: "notify-fallback-watcher",
7907
+ turn_count: 2,
7908
+ },
7909
+ },
7910
+ },
7911
+ },
7912
+ });
7913
+ const result = await dispatchCodexNativeHook({
7914
+ hook_event_name: "Stop",
7915
+ cwd,
7916
+ session_id: "sess-stop-skill-subagent-complete",
7917
+ }, { cwd });
7918
+ assert.equal(result.omxEventName, "stop");
7919
+ assert.equal(result.outputJson?.decision, "block");
7920
+ assert.doesNotMatch(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
7921
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
7922
+ }
7923
+ finally {
7924
+ await rm(cwd, { recursive: true, force: true });
7925
+ }
7926
+ });
7266
7927
  it("does not block on stale root ralplan skill when the explicit session-scoped canonical skill state is absent", async () => {
7267
7928
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-"));
7268
7929
  try {
@@ -10981,4 +11642,61 @@ describe("codex native hook triage integration", () => {
10981
11642
  }
10982
11643
  });
10983
11644
  });
11645
+ describe('native Stop autopilot deep-interview wait', () => {
11646
+ it('does not force continued execution while autopilot is waiting on a deep-interview omx question', async () => {
11647
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-native-hook-autopilot-question-wait-'));
11648
+ try {
11649
+ const sessionId = 'sess-autopilot-wait';
11650
+ const sessionDir = join(cwd, '.omx', 'state', 'sessions', sessionId);
11651
+ await writeJson(join(cwd, '.omx', 'state', 'session.json'), { session_id: sessionId });
11652
+ await writeJson(join(sessionDir, 'autopilot-state.json'), {
11653
+ mode: 'autopilot',
11654
+ active: true,
11655
+ current_phase: 'waiting-for-user',
11656
+ run_outcome: 'blocked_on_user',
11657
+ lifecycle_outcome: 'askuserQuestion',
11658
+ session_id: sessionId,
11659
+ state: {
11660
+ deep_interview_question: {
11661
+ status: 'waiting_for_user',
11662
+ source: 'omx-question',
11663
+ obligation_id: 'obligation-stop-1',
11664
+ previous_phase: 'deep-interview',
11665
+ },
11666
+ },
11667
+ });
11668
+ await writeJson(join(sessionDir, 'deep-interview-state.json'), {
11669
+ mode: 'deep-interview',
11670
+ active: false,
11671
+ current_phase: 'intent-first',
11672
+ lifecycle_outcome: 'askuserQuestion',
11673
+ run_outcome: 'blocked_on_user',
11674
+ session_id: sessionId,
11675
+ question_enforcement: {
11676
+ obligation_id: 'obligation-stop-1',
11677
+ source: 'omx-question',
11678
+ status: 'pending',
11679
+ lifecycle_outcome: 'askuserQuestion',
11680
+ requested_at: '2026-04-19T00:00:00.000Z',
11681
+ },
11682
+ });
11683
+ await writeJson(join(sessionDir, 'skill-active-state.json'), {
11684
+ active: true,
11685
+ skill: 'autopilot',
11686
+ phase: 'deep-interview',
11687
+ session_id: sessionId,
11688
+ active_skills: [{ skill: 'autopilot', phase: 'deep-interview', active: true, session_id: sessionId }],
11689
+ });
11690
+ const result = await dispatchCodexNativeHook({
11691
+ hook_event_name: 'Stop',
11692
+ session_id: sessionId,
11693
+ thread_id: 'thread-autopilot-wait',
11694
+ }, { cwd });
11695
+ assert.equal(result.outputJson, null);
11696
+ }
11697
+ finally {
11698
+ await rm(cwd, { recursive: true, force: true });
11699
+ }
11700
+ });
11701
+ });
10984
11702
  //# sourceMappingURL=codex-native-hook.test.js.map