oh-my-codex 0.18.1 → 0.18.3

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 (310) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +4 -2
  4. package/dist/agents/__tests__/definitions.test.js +23 -0
  5. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  6. package/dist/agents/__tests__/native-config.test.js +20 -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 +40 -0
  10. package/dist/agents/definitions.js.map +1 -1
  11. package/dist/agents/native-config.d.ts +1 -0
  12. package/dist/agents/native-config.d.ts.map +1 -1
  13. package/dist/agents/native-config.js +4 -0
  14. package/dist/agents/native-config.js.map +1 -1
  15. package/dist/auth/__tests__/config-sessions.test.d.ts +2 -0
  16. package/dist/auth/__tests__/config-sessions.test.d.ts.map +1 -0
  17. package/dist/auth/__tests__/config-sessions.test.js +48 -0
  18. package/dist/auth/__tests__/config-sessions.test.js.map +1 -0
  19. package/dist/auth/__tests__/quota-rotation.test.d.ts +2 -0
  20. package/dist/auth/__tests__/quota-rotation.test.d.ts.map +1 -0
  21. package/dist/auth/__tests__/quota-rotation.test.js +33 -0
  22. package/dist/auth/__tests__/quota-rotation.test.js.map +1 -0
  23. package/dist/auth/__tests__/redact.test.d.ts +2 -0
  24. package/dist/auth/__tests__/redact.test.d.ts.map +1 -0
  25. package/dist/auth/__tests__/redact.test.js +20 -0
  26. package/dist/auth/__tests__/redact.test.js.map +1 -0
  27. package/dist/auth/__tests__/storage.test.d.ts +2 -0
  28. package/dist/auth/__tests__/storage.test.d.ts.map +1 -0
  29. package/dist/auth/__tests__/storage.test.js +108 -0
  30. package/dist/auth/__tests__/storage.test.js.map +1 -0
  31. package/dist/auth/config.d.ts +9 -0
  32. package/dist/auth/config.d.ts.map +1 -0
  33. package/dist/auth/config.js +77 -0
  34. package/dist/auth/config.js.map +1 -0
  35. package/dist/auth/hotswap.d.ts +36 -0
  36. package/dist/auth/hotswap.d.ts.map +1 -0
  37. package/dist/auth/hotswap.js +159 -0
  38. package/dist/auth/hotswap.js.map +1 -0
  39. package/dist/auth/index.d.ts +8 -0
  40. package/dist/auth/index.d.ts.map +1 -0
  41. package/dist/auth/index.js +8 -0
  42. package/dist/auth/index.js.map +1 -0
  43. package/dist/auth/paths.d.ts +12 -0
  44. package/dist/auth/paths.d.ts.map +1 -0
  45. package/dist/auth/paths.js +78 -0
  46. package/dist/auth/paths.js.map +1 -0
  47. package/dist/auth/quota-detector.d.ts +10 -0
  48. package/dist/auth/quota-detector.d.ts.map +1 -0
  49. package/dist/auth/quota-detector.js +40 -0
  50. package/dist/auth/quota-detector.js.map +1 -0
  51. package/dist/auth/redact.d.ts +2 -0
  52. package/dist/auth/redact.d.ts.map +1 -0
  53. package/dist/auth/redact.js +26 -0
  54. package/dist/auth/redact.js.map +1 -0
  55. package/dist/auth/rotation.d.ts +9 -0
  56. package/dist/auth/rotation.d.ts.map +1 -0
  57. package/dist/auth/rotation.js +26 -0
  58. package/dist/auth/rotation.js.map +1 -0
  59. package/dist/auth/sessions.d.ts +15 -0
  60. package/dist/auth/sessions.d.ts.map +1 -0
  61. package/dist/auth/sessions.js +62 -0
  62. package/dist/auth/sessions.js.map +1 -0
  63. package/dist/auth/storage.d.ts +27 -0
  64. package/dist/auth/storage.d.ts.map +1 -0
  65. package/dist/auth/storage.js +111 -0
  66. package/dist/auth/storage.js.map +1 -0
  67. package/dist/catalog/__tests__/generator.test.js +4 -0
  68. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  69. package/dist/cli/__tests__/auth.test.d.ts +2 -0
  70. package/dist/cli/__tests__/auth.test.d.ts.map +1 -0
  71. package/dist/cli/__tests__/auth.test.js +168 -0
  72. package/dist/cli/__tests__/auth.test.js.map +1 -0
  73. package/dist/cli/__tests__/doctor-warning-copy.test.js +112 -5
  74. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  75. package/dist/cli/__tests__/explore.test.js +20 -0
  76. package/dist/cli/__tests__/explore.test.js.map +1 -1
  77. package/dist/cli/__tests__/index.test.js +171 -21
  78. package/dist/cli/__tests__/index.test.js.map +1 -1
  79. package/dist/cli/__tests__/launch-fallback.test.js +51 -3
  80. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  81. package/dist/cli/__tests__/nested-help-routing.test.js +1 -0
  82. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  83. package/dist/cli/__tests__/question.test.js +2 -2
  84. package/dist/cli/__tests__/question.test.js.map +1 -1
  85. package/dist/cli/__tests__/setup-agents-overwrite.test.js +30 -1
  86. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  87. package/dist/cli/__tests__/setup-install-mode.test.js +47 -0
  88. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  89. package/dist/cli/auth.d.ts +4 -0
  90. package/dist/cli/auth.d.ts.map +1 -0
  91. package/dist/cli/auth.js +89 -0
  92. package/dist/cli/auth.js.map +1 -0
  93. package/dist/cli/doctor.d.ts.map +1 -1
  94. package/dist/cli/doctor.js +190 -7
  95. package/dist/cli/doctor.js.map +1 -1
  96. package/dist/cli/explore.d.ts.map +1 -1
  97. package/dist/cli/explore.js +12 -0
  98. package/dist/cli/explore.js.map +1 -1
  99. package/dist/cli/index.d.ts +27 -3
  100. package/dist/cli/index.d.ts.map +1 -1
  101. package/dist/cli/index.js +245 -47
  102. package/dist/cli/index.js.map +1 -1
  103. package/dist/cli/setup.d.ts.map +1 -1
  104. package/dist/cli/setup.js +11 -3
  105. package/dist/cli/setup.js.map +1 -1
  106. package/dist/config/__tests__/codex-hooks.test.js +3 -3
  107. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  108. package/dist/config/__tests__/deep-interview.test.d.ts +2 -0
  109. package/dist/config/__tests__/deep-interview.test.d.ts.map +1 -0
  110. package/dist/config/__tests__/deep-interview.test.js +239 -0
  111. package/dist/config/__tests__/deep-interview.test.js.map +1 -0
  112. package/dist/config/__tests__/generator-idempotent.test.js +123 -0
  113. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  114. package/dist/config/codex-hooks.d.ts +1 -0
  115. package/dist/config/codex-hooks.d.ts.map +1 -1
  116. package/dist/config/codex-hooks.js +2 -4
  117. package/dist/config/codex-hooks.js.map +1 -1
  118. package/dist/config/deep-interview.d.ts +22 -0
  119. package/dist/config/deep-interview.d.ts.map +1 -0
  120. package/dist/config/deep-interview.js +151 -0
  121. package/dist/config/deep-interview.js.map +1 -0
  122. package/dist/config/generator.d.ts +19 -2
  123. package/dist/config/generator.d.ts.map +1 -1
  124. package/dist/config/generator.js +198 -29
  125. package/dist/config/generator.js.map +1 -1
  126. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +21 -0
  127. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  128. package/dist/goal-workflows/codex-goal-snapshot.d.ts +3 -0
  129. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  130. package/dist/goal-workflows/codex-goal-snapshot.js +45 -2
  131. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  132. package/dist/hooks/__tests__/agents-overlay.test.js +2 -0
  133. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  134. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +17 -0
  135. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  136. package/dist/hooks/__tests__/explore-routing.test.js +1 -0
  137. package/dist/hooks/__tests__/explore-routing.test.js.map +1 -1
  138. package/dist/hooks/__tests__/keyword-detector.test.js +471 -15
  139. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  140. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +2 -0
  141. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +1 -0
  142. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +320 -0
  143. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -0
  144. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +12 -0
  145. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  146. package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts +2 -0
  147. package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts.map +1 -0
  148. package/dist/hooks/__tests__/research-workflow-boundaries.test.js +35 -0
  149. package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -0
  150. package/dist/hooks/deep-interview-config-instruction.d.ts +3 -0
  151. package/dist/hooks/deep-interview-config-instruction.d.ts.map +1 -0
  152. package/dist/hooks/deep-interview-config-instruction.js +47 -0
  153. package/dist/hooks/deep-interview-config-instruction.js.map +1 -0
  154. package/dist/hooks/explore-routing.d.ts.map +1 -1
  155. package/dist/hooks/explore-routing.js +1 -0
  156. package/dist/hooks/explore-routing.js.map +1 -1
  157. package/dist/hooks/keyword-detector.d.ts +6 -1
  158. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  159. package/dist/hooks/keyword-detector.js +80 -14
  160. package/dist/hooks/keyword-detector.js.map +1 -1
  161. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  162. package/dist/hooks/keyword-registry.js +1 -0
  163. package/dist/hooks/keyword-registry.js.map +1 -1
  164. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  165. package/dist/hooks/prompt-guidance-contract.js +11 -0
  166. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  167. package/dist/hud/__tests__/hud-tmux-injection.test.js +22 -0
  168. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  169. package/dist/hud/__tests__/reconcile.test.js +213 -17
  170. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  171. package/dist/hud/__tests__/render.test.js +84 -0
  172. package/dist/hud/__tests__/render.test.js.map +1 -1
  173. package/dist/hud/__tests__/state.test.js +51 -1
  174. package/dist/hud/__tests__/state.test.js.map +1 -1
  175. package/dist/hud/__tests__/tmux.test.js +171 -23
  176. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  177. package/dist/hud/index.d.ts +1 -1
  178. package/dist/hud/index.d.ts.map +1 -1
  179. package/dist/hud/index.js +8 -3
  180. package/dist/hud/index.js.map +1 -1
  181. package/dist/hud/reconcile.d.ts +1 -1
  182. package/dist/hud/reconcile.d.ts.map +1 -1
  183. package/dist/hud/reconcile.js +14 -3
  184. package/dist/hud/reconcile.js.map +1 -1
  185. package/dist/hud/render.d.ts.map +1 -1
  186. package/dist/hud/render.js +26 -0
  187. package/dist/hud/render.js.map +1 -1
  188. package/dist/hud/state.d.ts +2 -1
  189. package/dist/hud/state.d.ts.map +1 -1
  190. package/dist/hud/state.js +62 -1
  191. package/dist/hud/state.js.map +1 -1
  192. package/dist/hud/tmux.d.ts +17 -3
  193. package/dist/hud/tmux.d.ts.map +1 -1
  194. package/dist/hud/tmux.js +96 -10
  195. package/dist/hud/tmux.js.map +1 -1
  196. package/dist/hud/types.d.ts +22 -0
  197. package/dist/hud/types.d.ts.map +1 -1
  198. package/dist/hud/types.js.map +1 -1
  199. package/dist/pipeline/__tests__/orchestrator.test.js +63 -1
  200. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  201. package/dist/pipeline/__tests__/stages.test.js +410 -4
  202. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  203. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  204. package/dist/pipeline/orchestrator.js +29 -2
  205. package/dist/pipeline/orchestrator.js.map +1 -1
  206. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  207. package/dist/pipeline/stages/ralplan.js +41 -6
  208. package/dist/pipeline/stages/ralplan.js.map +1 -1
  209. package/dist/question/__tests__/ui.test.js +43 -10
  210. package/dist/question/__tests__/ui.test.js.map +1 -1
  211. package/dist/question/deep-interview.d.ts +2 -0
  212. package/dist/question/deep-interview.d.ts.map +1 -1
  213. package/dist/question/deep-interview.js.map +1 -1
  214. package/dist/question/ui.d.ts +12 -0
  215. package/dist/question/ui.d.ts.map +1 -1
  216. package/dist/question/ui.js +83 -46
  217. package/dist/question/ui.js.map +1 -1
  218. package/dist/ralplan/__tests__/runtime.test.js +200 -10
  219. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  220. package/dist/ralplan/consensus-gate.d.ts +23 -0
  221. package/dist/ralplan/consensus-gate.d.ts.map +1 -0
  222. package/dist/ralplan/consensus-gate.js +212 -0
  223. package/dist/ralplan/consensus-gate.js.map +1 -0
  224. package/dist/ralplan/runtime.d.ts +25 -0
  225. package/dist/ralplan/runtime.d.ts.map +1 -1
  226. package/dist/ralplan/runtime.js +144 -8
  227. package/dist/ralplan/runtime.js.map +1 -1
  228. package/dist/scripts/__tests__/codex-native-hook.test.js +1034 -28
  229. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  230. package/dist/scripts/__tests__/docs-site-contract.test.d.ts +2 -0
  231. package/dist/scripts/__tests__/docs-site-contract.test.d.ts.map +1 -0
  232. package/dist/scripts/__tests__/docs-site-contract.test.js +42 -0
  233. package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -0
  234. package/dist/scripts/__tests__/notify-dispatcher.test.js +115 -2
  235. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  236. package/dist/scripts/__tests__/run-test-files.test.js +57 -0
  237. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  238. package/dist/scripts/__tests__/verify-native-agents.test.js +2 -2
  239. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  240. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  241. package/dist/scripts/codex-native-hook.js +238 -36
  242. package/dist/scripts/codex-native-hook.js.map +1 -1
  243. package/dist/scripts/notify-dispatcher.js +188 -4
  244. package/dist/scripts/notify-dispatcher.js.map +1 -1
  245. package/dist/scripts/run-test-files.js +13 -0
  246. package/dist/scripts/run-test-files.js.map +1 -1
  247. package/dist/state/__tests__/planning-gate.test.d.ts +2 -0
  248. package/dist/state/__tests__/planning-gate.test.d.ts.map +1 -0
  249. package/dist/state/__tests__/planning-gate.test.js +219 -0
  250. package/dist/state/__tests__/planning-gate.test.js.map +1 -0
  251. package/dist/state/__tests__/workflow-transition.test.js +6 -0
  252. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  253. package/dist/state/workflow-transition.d.ts +24 -1
  254. package/dist/state/workflow-transition.d.ts.map +1 -1
  255. package/dist/state/workflow-transition.js +70 -0
  256. package/dist/state/workflow-transition.js.map +1 -1
  257. package/dist/subagents/tracker.d.ts.map +1 -1
  258. package/dist/subagents/tracker.js +4 -3
  259. package/dist/subagents/tracker.js.map +1 -1
  260. package/dist/team/__tests__/runtime.test.js +36 -44
  261. package/dist/team/__tests__/runtime.test.js.map +1 -1
  262. package/dist/team/__tests__/tmux-session.test.js +144 -18
  263. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  264. package/dist/team/runtime.d.ts.map +1 -1
  265. package/dist/team/runtime.js +10 -20
  266. package/dist/team/runtime.js.map +1 -1
  267. package/dist/team/tmux-session.d.ts.map +1 -1
  268. package/dist/team/tmux-session.js +22 -6
  269. package/dist/team/tmux-session.js.map +1 -1
  270. package/dist/ultragoal/__tests__/artifacts.test.js +50 -0
  271. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  272. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  273. package/dist/ultragoal/artifacts.js +28 -2
  274. package/dist/ultragoal/artifacts.js.map +1 -1
  275. package/package.json +1 -1
  276. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  277. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +16 -4
  278. package/plugins/oh-my-codex/skills/autoresearch/SKILL.md +4 -0
  279. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +1 -1
  280. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +1 -1
  281. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +10 -0
  282. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +1 -1
  283. package/plugins/oh-my-codex/skills/plan/SKILL.md +1 -1
  284. package/plugins/oh-my-codex/skills/prometheus-strict/README.md +35 -0
  285. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +219 -0
  286. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +24 -5
  287. package/prompts/prometheus-strict-metis.md +274 -0
  288. package/prompts/prometheus-strict-momus.md +82 -0
  289. package/prompts/prometheus-strict-oracle.md +107 -0
  290. package/prompts/researcher.md +22 -3
  291. package/prompts/scholastic.md +11 -0
  292. package/skills/autopilot/SKILL.md +16 -4
  293. package/skills/autoresearch/SKILL.md +4 -0
  294. package/skills/autoresearch-goal/SKILL.md +1 -1
  295. package/skills/best-practice-research/SKILL.md +1 -1
  296. package/skills/deep-interview/SKILL.md +10 -0
  297. package/skills/pipeline/SKILL.md +1 -1
  298. package/skills/plan/SKILL.md +1 -1
  299. package/skills/prometheus-strict/README.md +35 -0
  300. package/skills/prometheus-strict/SKILL.md +219 -0
  301. package/skills/ralplan/SKILL.md +24 -5
  302. package/src/scripts/__tests__/codex-native-hook.test.ts +1307 -61
  303. package/src/scripts/__tests__/docs-site-contract.test.ts +47 -0
  304. package/src/scripts/__tests__/notify-dispatcher.test.ts +132 -3
  305. package/src/scripts/__tests__/run-test-files.test.ts +67 -0
  306. package/src/scripts/__tests__/verify-native-agents.test.ts +2 -2
  307. package/src/scripts/codex-native-hook.ts +260 -31
  308. package/src/scripts/notify-dispatcher.ts +202 -4
  309. package/src/scripts/run-test-files.ts +13 -0
  310. package/templates/catalog-manifest.json +27 -0
@@ -25,10 +25,13 @@ 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";
30
+ import { renderHud } from "../../hud/render.js";
29
31
  import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
30
32
  import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
31
33
  import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
34
+ import { getBaseStateDir } from "../../state/paths.js";
32
35
 
33
36
  function nativeHookScriptPath(): string {
34
37
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
@@ -63,6 +66,19 @@ async function writeJson(path: string, value: unknown): Promise<void> {
63
66
  await writeFile(path, JSON.stringify(value, null, 2));
64
67
  }
65
68
 
69
+ async function withIsolatedHome<T>(prefix: string, run: (homeDir: string) => Promise<T>): Promise<T> {
70
+ const homeDir = await mkdtemp(join(tmpdir(), `omx-native-hook-home-${prefix}-`));
71
+ const previousHome = process.env.HOME;
72
+ try {
73
+ process.env.HOME = homeDir;
74
+ return await run(homeDir);
75
+ } finally {
76
+ if (typeof previousHome === "string") process.env.HOME = previousHome;
77
+ else delete process.env.HOME;
78
+ await rm(homeDir, { recursive: true, force: true });
79
+ }
80
+ }
81
+
66
82
  async function withLoreGuardConfig<T>(
67
83
  value: string,
68
84
  prefix: string,
@@ -285,7 +301,7 @@ describe("codex native hook config", () => {
285
301
  matcher?: string;
286
302
  hooks?: Array<Record<string, unknown>>;
287
303
  };
288
- assert.equal(preToolUse.matcher, "Bash");
304
+ assert.equal(preToolUse.matcher, undefined);
289
305
  assert.match(
290
306
  String(preToolUse.hooks?.[0]?.command || ""),
291
307
  /codex-native-hook\.js"?$/,
@@ -1618,6 +1634,405 @@ describe("codex native hook dispatch", () => {
1618
1634
  }
1619
1635
  });
1620
1636
 
1637
+ it("injects deep-interview config overrides into UserPromptSubmit developer context", async () => {
1638
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-"));
1639
+ try {
1640
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1641
+ await writeFile(
1642
+ join(cwd, ".omx", "config.toml"),
1643
+ `[omx.deepInterview]
1644
+ defaultProfile = "standard"
1645
+ standardThreshold = 0.05
1646
+ standardMaxRounds = 15
1647
+ enableChallengeModes = false
1648
+ `,
1649
+ );
1650
+
1651
+ const result = await dispatchCodexNativeHook(
1652
+ {
1653
+ hook_event_name: "UserPromptSubmit",
1654
+ cwd,
1655
+ session_id: "sess-deep-interview-config",
1656
+ thread_id: "thread-1",
1657
+ turn_id: "turn-1",
1658
+ prompt: "$deep-interview prove config reflection",
1659
+ },
1660
+ { cwd },
1661
+ );
1662
+
1663
+ assert.equal(result.omxEventName, "keyword-detector");
1664
+ assert.equal(result.skillState?.skill, "deep-interview");
1665
+ const serializedOutput = JSON.stringify(result.outputJson);
1666
+ assert.match(serializedOutput, /Deep-interview config override active/);
1667
+ assert.match(serializedOutput, /threshold=0\.05/);
1668
+ assert.match(serializedOutput, /max_rounds=15/);
1669
+ assert.match(serializedOutput, /enableChallengeModes=false/);
1670
+
1671
+ const modeState = JSON.parse(
1672
+ await readFile(join(cwd, ".omx", "state", "sessions", "sess-deep-interview-config", "deep-interview-state.json"), "utf-8"),
1673
+ ) as { threshold?: number; max_rounds?: number; profile?: string };
1674
+ assert.equal(modeState.profile, "standard");
1675
+ assert.equal(modeState.threshold, 0.05);
1676
+ assert.equal(modeState.max_rounds, 15);
1677
+ } finally {
1678
+ await rm(cwd, { recursive: true, force: true });
1679
+ }
1680
+ });
1681
+
1682
+ it("proves UserPromptSubmit context changes before and after adding deep-interview config", async () => {
1683
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-before-after-"));
1684
+ const sessionId = "sess-deep-interview-config-before-after";
1685
+ try {
1686
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1687
+
1688
+ const before = await withIsolatedHome("deep-interview-config-before-after", async () => (
1689
+ dispatchCodexNativeHook(
1690
+ {
1691
+ hook_event_name: "UserPromptSubmit",
1692
+ cwd,
1693
+ session_id: sessionId,
1694
+ thread_id: "thread-before-after",
1695
+ turn_id: "turn-before",
1696
+ prompt: "$deep-interview prove before config context",
1697
+ },
1698
+ { cwd },
1699
+ )
1700
+ ));
1701
+ const beforeOutput = JSON.stringify(before.outputJson);
1702
+ const beforeState = JSON.parse(
1703
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1704
+ ) as {
1705
+ deep_interview_config?: unknown;
1706
+ threshold?: number;
1707
+ max_rounds?: number;
1708
+ };
1709
+ assert.equal(before.skillState?.skill, "deep-interview");
1710
+ assert.doesNotMatch(beforeOutput, /Deep-interview config override active/);
1711
+ assert.equal(before.skillState?.deep_interview_config, undefined);
1712
+ assert.equal(beforeState.deep_interview_config, undefined);
1713
+ assert.equal(beforeState.threshold, undefined);
1714
+ assert.equal(beforeState.max_rounds, undefined);
1715
+
1716
+ await writeFile(
1717
+ join(cwd, ".omx", "config.toml"),
1718
+ `[omx.deepInterview]
1719
+ defaultProfile = "standard"
1720
+ standardThreshold = 0.05
1721
+ standardMaxRounds = 15
1722
+ `,
1723
+ );
1724
+
1725
+ const after = await dispatchCodexNativeHook(
1726
+ {
1727
+ hook_event_name: "UserPromptSubmit",
1728
+ cwd,
1729
+ session_id: sessionId,
1730
+ thread_id: "thread-before-after",
1731
+ turn_id: "turn-after",
1732
+ prompt: "$deep-interview prove after config context",
1733
+ },
1734
+ { cwd },
1735
+ );
1736
+ const afterOutput = JSON.stringify(after.outputJson);
1737
+ const afterState = JSON.parse(
1738
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1739
+ ) as {
1740
+ deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number };
1741
+ threshold?: number;
1742
+ max_rounds?: number;
1743
+ };
1744
+ assert.equal(after.skillState?.deep_interview_config?.profile, "standard");
1745
+ assert.match(afterOutput, /Deep-interview config override active/);
1746
+ assert.match(afterOutput, /threshold=0\.05/);
1747
+ assert.match(afterOutput, /max_rounds=15/);
1748
+ assert.equal(afterState.deep_interview_config?.profile, "standard");
1749
+ assert.equal(afterState.threshold, 0.05);
1750
+ assert.equal(afterState.max_rounds, 15);
1751
+ } finally {
1752
+ await rm(cwd, { recursive: true, force: true });
1753
+ }
1754
+ });
1755
+
1756
+ it("injects deep-interview config for mixed workflow prompts that defer execution modes", async () => {
1757
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-mixed-"));
1758
+ const sessionId = "sess-deep-interview-config-mixed";
1759
+ try {
1760
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1761
+ await writeFile(
1762
+ join(cwd, ".omx", "config.toml"),
1763
+ `[omx.deepInterview]
1764
+ defaultProfile = "deep"
1765
+ deepThreshold = 0.13
1766
+ deepMaxRounds = 21
1767
+ enableChallengeModes = false
1768
+ `,
1769
+ );
1770
+
1771
+ const result = await withIsolatedHome("deep-interview-config-mixed", async () => (
1772
+ dispatchCodexNativeHook(
1773
+ {
1774
+ hook_event_name: "UserPromptSubmit",
1775
+ cwd,
1776
+ session_id: sessionId,
1777
+ thread_id: "thread-mixed-config",
1778
+ turn_id: "turn-mixed-config",
1779
+ prompt: "$autopilot $deep-interview prove mixed config context",
1780
+ },
1781
+ { cwd },
1782
+ )
1783
+ ));
1784
+ const serializedOutput = JSON.stringify(result.outputJson);
1785
+ const modeState = JSON.parse(
1786
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1787
+ ) as {
1788
+ deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number; enableChallengeModes?: boolean };
1789
+ profile?: string;
1790
+ threshold?: number;
1791
+ max_rounds?: number;
1792
+ enable_challenge_modes?: boolean;
1793
+ };
1794
+
1795
+ assert.equal(result.skillState?.skill, "deep-interview");
1796
+ assert.deepEqual(result.skillState?.deferred_skills, ["autopilot"]);
1797
+ assert.equal(result.skillState?.deep_interview_config?.profile, "deep");
1798
+ assert.equal(result.skillState?.deep_interview_config?.threshold, 0.13);
1799
+ assert.equal(result.skillState?.deep_interview_config?.maxRounds, 21);
1800
+ assert.equal(result.skillState?.deep_interview_config?.enableChallengeModes, false);
1801
+ assert.match(serializedOutput, /Deep-interview config override active/);
1802
+ assert.match(serializedOutput, /profile=deep/);
1803
+ assert.match(serializedOutput, /threshold=0\.13/);
1804
+ assert.match(serializedOutput, /max_rounds=21/);
1805
+ assert.match(serializedOutput, /enableChallengeModes=false/);
1806
+ assert.equal(modeState.deep_interview_config?.profile, "deep");
1807
+ assert.equal(modeState.profile, "deep");
1808
+ assert.equal(modeState.threshold, 0.13);
1809
+ assert.equal(modeState.max_rounds, 21);
1810
+ assert.equal(modeState.enable_challenge_modes, false);
1811
+ } finally {
1812
+ await rm(cwd, { recursive: true, force: true });
1813
+ }
1814
+ });
1815
+
1816
+ it("keeps deep-interview config override context on continuation prompts", async () => {
1817
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-continuation-"));
1818
+ const sessionId = "sess-deep-interview-config-continuation";
1819
+ try {
1820
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1821
+ await writeFile(
1822
+ join(cwd, ".omx", "config.toml"),
1823
+ `[omx.deepInterview]
1824
+ defaultProfile = "standard"
1825
+ standardThreshold = 0.05
1826
+ standardMaxRounds = 15
1827
+ `,
1828
+ );
1829
+
1830
+ await dispatchCodexNativeHook(
1831
+ {
1832
+ hook_event_name: "UserPromptSubmit",
1833
+ cwd,
1834
+ session_id: sessionId,
1835
+ thread_id: "thread-continuation",
1836
+ turn_id: "turn-start",
1837
+ prompt: "$deep-interview prove config continuation",
1838
+ },
1839
+ { cwd },
1840
+ );
1841
+ const continued = await dispatchCodexNativeHook(
1842
+ {
1843
+ hook_event_name: "UserPromptSubmit",
1844
+ cwd,
1845
+ session_id: sessionId,
1846
+ thread_id: "thread-continuation",
1847
+ turn_id: "turn-continue",
1848
+ prompt: "continue",
1849
+ },
1850
+ { cwd },
1851
+ );
1852
+ const serializedOutput = JSON.stringify(continued.outputJson);
1853
+ const modeState = JSON.parse(
1854
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1855
+ ) as { threshold?: number; max_rounds?: number; profile?: string };
1856
+
1857
+ assert.equal(continued.skillState?.skill, "deep-interview");
1858
+ assert.match(serializedOutput, /Deep-interview config override active/);
1859
+ assert.match(serializedOutput, /threshold=0\.05/);
1860
+ assert.match(serializedOutput, /max_rounds=15/);
1861
+ assert.equal(modeState.profile, "standard");
1862
+ assert.equal(modeState.threshold, 0.05);
1863
+ assert.equal(modeState.max_rounds, 15);
1864
+ } finally {
1865
+ await rm(cwd, { recursive: true, force: true });
1866
+ }
1867
+ });
1868
+
1869
+ it("keeps explicit deep-interview profile flags reflected on continuation prompts", async () => {
1870
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-profile-continuation-"));
1871
+ const sessionId = "sess-deep-interview-config-profile-continuation";
1872
+ try {
1873
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1874
+ await writeFile(
1875
+ join(cwd, ".omx", "config.toml"),
1876
+ `[omx.deepInterview]
1877
+ defaultProfile = "standard"
1878
+ standardThreshold = 0.22
1879
+ standardMaxRounds = 13
1880
+ deepThreshold = 0.13
1881
+ deepMaxRounds = 21
1882
+ `,
1883
+ );
1884
+
1885
+ await dispatchCodexNativeHook(
1886
+ {
1887
+ hook_event_name: "UserPromptSubmit",
1888
+ cwd,
1889
+ session_id: sessionId,
1890
+ thread_id: "thread-profile-continuation",
1891
+ turn_id: "turn-start",
1892
+ prompt: "$deep-interview --deep prove explicit profile continuation",
1893
+ },
1894
+ { cwd },
1895
+ );
1896
+ const continued = await dispatchCodexNativeHook(
1897
+ {
1898
+ hook_event_name: "UserPromptSubmit",
1899
+ cwd,
1900
+ session_id: sessionId,
1901
+ thread_id: "thread-profile-continuation",
1902
+ turn_id: "turn-continue",
1903
+ prompt: "continue",
1904
+ },
1905
+ { cwd },
1906
+ );
1907
+ const serializedOutput = JSON.stringify(continued.outputJson);
1908
+ const modeState = JSON.parse(
1909
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1910
+ ) as { threshold?: number; max_rounds?: number; profile?: string; deep_interview_config?: { profile?: string } };
1911
+
1912
+ assert.equal(continued.skillState?.skill, "deep-interview");
1913
+ assert.equal(continued.skillState?.deep_interview_config?.profile, "deep");
1914
+ assert.match(serializedOutput, /Deep-interview config override active/);
1915
+ assert.match(serializedOutput, /profile=deep/);
1916
+ assert.match(serializedOutput, /threshold=0\.13/);
1917
+ assert.match(serializedOutput, /max_rounds=21/);
1918
+ assert.equal(modeState.deep_interview_config?.profile, "deep");
1919
+ assert.equal(modeState.profile, "deep");
1920
+ assert.equal(modeState.threshold, 0.13);
1921
+ assert.equal(modeState.max_rounds, 21);
1922
+ } finally {
1923
+ await rm(cwd, { recursive: true, force: true });
1924
+ }
1925
+ });
1926
+
1927
+ it("keeps the documented deep-interview Suggested Config reflected in UserPromptSubmit context", async () => {
1928
+ const skillDoc = await readFile(join(process.cwd(), "skills", "deep-interview", "SKILL.md"), "utf-8");
1929
+ const markerIndex = skillDoc.indexOf("## Suggested Config (optional)");
1930
+ assert.notEqual(markerIndex, -1);
1931
+ const configMatch = skillDoc.slice(markerIndex).match(/```toml\n([\s\S]*?)\n```/);
1932
+ assert.ok(configMatch);
1933
+ const documentedConfig = configMatch[1]?.trimEnd();
1934
+ assert.ok(documentedConfig);
1935
+ assert.match(documentedConfig, /standardThreshold = 0\.20/);
1936
+ assert.match(documentedConfig, /standardMaxRounds = 12/);
1937
+
1938
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-doc-config-"));
1939
+ const sessionId = "sess-deep-interview-doc-config";
1940
+ try {
1941
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1942
+ await writeFile(join(cwd, ".omx", "config.toml"), `${documentedConfig}\n`);
1943
+
1944
+ const result = await dispatchCodexNativeHook(
1945
+ {
1946
+ hook_event_name: "UserPromptSubmit",
1947
+ cwd,
1948
+ session_id: sessionId,
1949
+ thread_id: "thread-doc-config",
1950
+ turn_id: "turn-doc-config",
1951
+ prompt: "$deep-interview prove documented config context",
1952
+ },
1953
+ { cwd },
1954
+ );
1955
+ const serializedOutput = JSON.stringify(result.outputJson);
1956
+ const modeState = JSON.parse(
1957
+ await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
1958
+ ) as {
1959
+ deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number };
1960
+ profile?: string;
1961
+ threshold?: number;
1962
+ max_rounds?: number;
1963
+ };
1964
+
1965
+ assert.equal(result.skillState?.deep_interview_config?.profile, "standard");
1966
+ assert.equal(result.skillState?.deep_interview_config?.threshold, 0.2);
1967
+ assert.equal(result.skillState?.deep_interview_config?.maxRounds, 12);
1968
+ assert.match(serializedOutput, /Deep-interview config override active/);
1969
+ assert.match(serializedOutput, /profile=standard/);
1970
+ assert.match(serializedOutput, /threshold=0\.2/);
1971
+ assert.match(serializedOutput, /max_rounds=12/);
1972
+ assert.equal(modeState.deep_interview_config?.profile, "standard");
1973
+ assert.equal(modeState.profile, "standard");
1974
+ assert.equal(modeState.threshold, 0.2);
1975
+ assert.equal(modeState.max_rounds, 12);
1976
+ } finally {
1977
+ await rm(cwd, { recursive: true, force: true });
1978
+ }
1979
+ });
1980
+
1981
+ it("injects deep-interview config overrides when state is boxed under OMX_ROOT", async () => {
1982
+ const root = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-boxed-"));
1983
+ const cwd = join(root, "source");
1984
+ const omxRoot = join(root, "box");
1985
+ const sessionId = "sess-boxed-deep-interview-config";
1986
+ const previousOmxRoot = process.env.OMX_ROOT;
1987
+ const previousOmxStateRoot = process.env.OMX_STATE_ROOT;
1988
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1989
+ try {
1990
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1991
+ await writeFile(
1992
+ join(cwd, ".omx", "config.toml"),
1993
+ `[omx.deepInterview]
1994
+ defaultProfile = "standard"
1995
+ standardThreshold = 0.05
1996
+ standardMaxRounds = 15
1997
+ `,
1998
+ );
1999
+ process.env.OMX_ROOT = omxRoot;
2000
+ delete process.env.OMX_STATE_ROOT;
2001
+ delete process.env.OMX_TEAM_STATE_ROOT;
2002
+
2003
+ const result = await dispatchCodexNativeHook(
2004
+ {
2005
+ hook_event_name: "UserPromptSubmit",
2006
+ cwd,
2007
+ session_id: sessionId,
2008
+ thread_id: "thread-boxed",
2009
+ turn_id: "turn-boxed",
2010
+ prompt: "$deep-interview prove boxed config reflection",
2011
+ },
2012
+ { cwd },
2013
+ );
2014
+
2015
+ assert.equal(result.omxEventName, "keyword-detector");
2016
+ assert.equal(result.skillState?.initialized_state_path, `.omx/state/sessions/${sessionId}/deep-interview-state.json`);
2017
+ const boxedStatePath = join(omxRoot, ".omx", "state", "sessions", sessionId, "deep-interview-state.json");
2018
+ assert.equal(existsSync(boxedStatePath), true);
2019
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json")), false);
2020
+
2021
+ const serializedOutput = JSON.stringify(result.outputJson);
2022
+ assert.match(serializedOutput, /Deep-interview config override active/);
2023
+ assert.match(serializedOutput, /threshold=0\.05/);
2024
+ assert.match(serializedOutput, /max_rounds=15/);
2025
+ } finally {
2026
+ if (typeof previousOmxRoot === "string") process.env.OMX_ROOT = previousOmxRoot;
2027
+ else delete process.env.OMX_ROOT;
2028
+ if (typeof previousOmxStateRoot === "string") process.env.OMX_STATE_ROOT = previousOmxStateRoot;
2029
+ else delete process.env.OMX_STATE_ROOT;
2030
+ if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
2031
+ else delete process.env.OMX_TEAM_STATE_ROOT;
2032
+ await rm(root, { recursive: true, force: true });
2033
+ }
2034
+ });
2035
+
1621
2036
  it("records boxed keyword activation mode detail and skill state under OMX_ROOT", async () => {
1622
2037
  const root = await mkdtemp(join(tmpdir(), "omx-native-hook-boxed-"));
1623
2038
  const cwd = join(root, "source");
@@ -1937,6 +2352,9 @@ describe("codex native hook dispatch", () => {
1937
2352
  assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
1938
2353
  assert.match(output, /--status blocked/);
1939
2354
  assert.match(output, /Codex goal context/);
2355
+ assert.match(output, /no such table: thread_goals/);
2356
+ assert.match(output, /unavailable get_goal error JSON or path/);
2357
+ assert.match(output, /safe-recovery blocker/);
1940
2358
  assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
1941
2359
  assert.match(output, /Hooks must not mutate Codex goal state/);
1942
2360
  } finally {
@@ -2233,6 +2651,36 @@ describe("codex native hook dispatch", () => {
2233
2651
  }
2234
2652
  });
2235
2653
 
2654
+ it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
2655
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
2656
+ try {
2657
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2658
+ const result = await dispatchCodexNativeHook(
2659
+ {
2660
+ hook_event_name: "UserPromptSubmit",
2661
+ cwd,
2662
+ session_id: "sess-autopilot-ralplan-gate",
2663
+ thread_id: "thread-autopilot-ralplan-gate",
2664
+ turn_id: "turn-autopilot-ralplan-gate",
2665
+ prompt: "$autopilot implement issue #2430",
2666
+ },
2667
+ { cwd },
2668
+ );
2669
+
2670
+ assert.equal(result.omxEventName, "keyword-detector");
2671
+ assert.equal(result.skillState?.skill, "autopilot");
2672
+ const message = String(
2673
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
2674
+ );
2675
+ assert.match(message, /Autopilot protocol:/);
2676
+ assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
2677
+ assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
2678
+ assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
2679
+ } finally {
2680
+ await rm(cwd, { recursive: true, force: true });
2681
+ }
2682
+ });
2683
+
2236
2684
  it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
2237
2685
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
2238
2686
  try {
@@ -2251,7 +2699,11 @@ describe("codex native hook dispatch", () => {
2251
2699
 
2252
2700
  assert.equal(result.omxEventName, "keyword-detector");
2253
2701
  assert.equal(result.skillState?.skill, "ultragoal");
2254
- assert.equal(result.skillState?.initialized_mode, undefined);
2702
+ assert.equal(result.skillState?.initialized_mode, "ultragoal");
2703
+ assert.equal(
2704
+ result.skillState?.initialized_state_path,
2705
+ ".omx/state/sessions/sess-ultragoal-1/ultragoal-state.json",
2706
+ );
2255
2707
  const message = String(
2256
2708
  (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
2257
2709
  );
@@ -2262,40 +2714,112 @@ describe("codex native hook dispatch", () => {
2262
2714
  assert.match(message, /update_goal/);
2263
2715
  assert.match(message, /does not call `\/goal clear`/);
2264
2716
  assert.match(message, /multiple sequential ultragoal runs/);
2265
- assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
2717
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), true);
2266
2718
  } finally {
2267
2719
  await rm(cwd, { recursive: true, force: true });
2268
2720
  }
2269
2721
  });
2270
2722
 
2271
- it("applies only explicit structured UserPromptSubmit ultragoal steering directives", async () => {
2272
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-"));
2723
+ it("deactivates active deep-interview state on explicit ultragoal handoff", async () => {
2724
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-deep-interview-handoff-"));
2273
2725
  try {
2274
- await createUltragoalPlan(cwd, {
2275
- brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal hook steering fixture",
2276
- goals: [{ title: "First", objective: "Complete first milestone with tests." }],
2726
+ const stateDir = join(cwd, ".omx", "state");
2727
+ const sessionDir = join(stateDir, "sessions", "sess-ultragoal-handoff");
2728
+ await mkdir(sessionDir, { recursive: true });
2729
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
2730
+ version: 1,
2731
+ active: true,
2732
+ skill: "deep-interview",
2733
+ phase: "planning",
2734
+ session_id: "sess-ultragoal-handoff",
2735
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-ultragoal-handoff" }],
2736
+ });
2737
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
2738
+ active: true,
2739
+ mode: "deep-interview",
2740
+ current_phase: "intent-first",
2741
+ session_id: "sess-ultragoal-handoff",
2742
+ question_enforcement: {
2743
+ obligation_id: "obligation-ultragoal-handoff",
2744
+ source: "omx-question",
2745
+ status: "pending",
2746
+ requested_at: "2026-05-21T03:00:00.000Z",
2747
+ },
2277
2748
  });
2278
2749
 
2279
- const prose = await dispatchCodexNativeHook(
2750
+ const result = await dispatchCodexNativeHook(
2280
2751
  {
2281
2752
  hook_event_name: "UserPromptSubmit",
2282
2753
  cwd,
2283
- session_id: "sess-ultragoal-steer-1",
2284
- prompt: "Please add a subgoal for docs later; this is normal prose, not a directive.",
2754
+ session_id: "sess-ultragoal-handoff",
2755
+ thread_id: "thread-ultragoal-handoff",
2756
+ turn_id: "turn-ultragoal-handoff",
2757
+ prompt: "$ultragoal turn the clarified spec into goals",
2285
2758
  },
2286
2759
  { cwd },
2287
2760
  );
2288
- assert.equal(prose.outputJson, null);
2289
- assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
2290
2761
 
2291
- const jsonExample = await dispatchCodexNativeHook(
2762
+ assert.equal(result.omxEventName, "keyword-detector");
2763
+ assert.equal(result.skillState?.skill, "ultragoal");
2764
+ assert.match(JSON.stringify(result.outputJson), /mode transiting: deep-interview -> ultragoal/);
2765
+
2766
+ const completed = JSON.parse(await readFile(join(sessionDir, "deep-interview-state.json"), "utf-8")) as {
2767
+ active?: boolean;
2768
+ current_phase?: string;
2769
+ question_enforcement?: { status?: string; clear_reason?: string };
2770
+ };
2771
+ assert.equal(completed.active, false);
2772
+ assert.equal(completed.current_phase, "completed");
2773
+ assert.equal(completed.question_enforcement?.status, "cleared");
2774
+ assert.equal(completed.question_enforcement?.clear_reason, "handoff");
2775
+ assert.equal(existsSync(join(sessionDir, "ultragoal-state.json")), true);
2776
+
2777
+ const edit = await dispatchCodexNativeHook(
2292
2778
  {
2293
- hook_event_name: "UserPromptSubmit",
2779
+ hook_event_name: "PreToolUse",
2294
2780
  cwd,
2295
- session_id: "sess-ultragoal-steer-1",
2296
- prompt: `Here is an inert example:\n\`\`\`json\n${JSON.stringify({
2297
- kind: "add_subgoal",
2298
- source: "user_prompt_submit",
2781
+ session_id: "sess-ultragoal-handoff",
2782
+ thread_id: "thread-ultragoal-handoff",
2783
+ tool_name: "Edit",
2784
+ tool_use_id: "tool-ultragoal-post-handoff-edit",
2785
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
2786
+ },
2787
+ { cwd },
2788
+ );
2789
+ assert.equal(edit.outputJson, null);
2790
+ } finally {
2791
+ await rm(cwd, { recursive: true, force: true });
2792
+ }
2793
+ });
2794
+
2795
+ it("applies only explicit structured UserPromptSubmit ultragoal steering directives", async () => {
2796
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-"));
2797
+ try {
2798
+ await createUltragoalPlan(cwd, {
2799
+ brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal hook steering fixture",
2800
+ goals: [{ title: "First", objective: "Complete first milestone with tests." }],
2801
+ });
2802
+
2803
+ const prose = await dispatchCodexNativeHook(
2804
+ {
2805
+ hook_event_name: "UserPromptSubmit",
2806
+ cwd,
2807
+ session_id: "sess-ultragoal-steer-1",
2808
+ prompt: "Please add a subgoal for docs later; this is normal prose, not a directive.",
2809
+ },
2810
+ { cwd },
2811
+ );
2812
+ assert.equal(prose.outputJson, null);
2813
+ assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
2814
+
2815
+ const jsonExample = await dispatchCodexNativeHook(
2816
+ {
2817
+ hook_event_name: "UserPromptSubmit",
2818
+ cwd,
2819
+ session_id: "sess-ultragoal-steer-1",
2820
+ prompt: `Here is an inert example:\n\`\`\`json\n${JSON.stringify({
2821
+ kind: "add_subgoal",
2822
+ source: "user_prompt_submit",
2299
2823
  evidence: "Example JSON should not mutate .omx/ultragoal.",
2300
2824
  rationale: "Only explicit steering fences or labels are executable.",
2301
2825
  title: "Inert JSON example",
@@ -3410,6 +3934,78 @@ esac
3410
3934
  }
3411
3935
  });
3412
3936
 
3937
+ it("reuses an existing owner-tagged HUD pane when UserPromptSubmit revives with the canonical session id", async () => {
3938
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-"));
3939
+ const originalTmux = process.env.TMUX;
3940
+ const originalTmuxPane = process.env.TMUX_PANE;
3941
+ const originalPath = process.env.PATH;
3942
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
3943
+ try {
3944
+ process.env.TMUX = "1";
3945
+ process.env.TMUX_PANE = "%1";
3946
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3947
+ const canonicalSessionId = "omx-canonical-hud-reuse";
3948
+ const nativeSessionId = "codex-native-hud-reuse";
3949
+ await mkdir(join(cwd, ".omx", "state", "sessions", canonicalSessionId), { recursive: true });
3950
+ await writeSessionStart(cwd, canonicalSessionId);
3951
+
3952
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-bin-"));
3953
+ const tmuxLog = join(cwd, "tmux.log");
3954
+ await writeFile(
3955
+ join(binDir, "tmux"),
3956
+ `#!/usr/bin/env bash
3957
+ set -euo pipefail
3958
+ printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
3959
+ case "$1" in
3960
+ list-panes)
3961
+ printf '%%1\tcodex\tcodex\n'
3962
+ printf '%%2\tnode\texec env OMX_TMUX_HUD_OWNER='"'"'1'"'"' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='"'"'%%1'"'"' /node /omx.js hud --watch\n'
3963
+ ;;
3964
+ display-message)
3965
+ printf '80\t24\n'
3966
+ ;;
3967
+ resize-pane)
3968
+ ;;
3969
+ split-window)
3970
+ printf '%%9\n'
3971
+ ;;
3972
+ esac
3973
+ `,
3974
+ );
3975
+ await chmod(join(binDir, "tmux"), 0o755);
3976
+ process.env.PATH = `${binDir}:${originalPath}`;
3977
+
3978
+ const result = await dispatchCodexNativeHook(
3979
+ {
3980
+ hook_event_name: "UserPromptSubmit",
3981
+ cwd,
3982
+ session_id: nativeSessionId,
3983
+ thread_id: "thread-hud-reuse",
3984
+ turn_id: "turn-hud-reuse",
3985
+ prompt: "$ralplan prepare plan",
3986
+ },
3987
+ { cwd },
3988
+ );
3989
+
3990
+ assert.equal(result.omxEventName, "keyword-detector");
3991
+ const tmuxCalls = await readFile(tmuxLog, "utf-8");
3992
+ assert.match(tmuxCalls, /list-panes -t %1 -F/);
3993
+ assert.match(tmuxCalls, /resize-pane -t %2 -y 3/);
3994
+ assert.doesNotMatch(tmuxCalls, /split-window/);
3995
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", canonicalSessionId, "ralplan-state.json")), true);
3996
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", nativeSessionId, "ralplan-state.json")), false);
3997
+ } finally {
3998
+ if (originalTmux === undefined) delete process.env.TMUX;
3999
+ else process.env.TMUX = originalTmux;
4000
+ if (originalTmuxPane === undefined) delete process.env.TMUX_PANE;
4001
+ else process.env.TMUX_PANE = originalTmuxPane;
4002
+ if (originalHudOwner === undefined) delete process.env[OMX_TMUX_HUD_OWNER_ENV];
4003
+ else process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
4004
+ process.env.PATH = originalPath;
4005
+ await rm(cwd, { recursive: true, force: true });
4006
+ }
4007
+ });
4008
+
3413
4009
  it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
3414
4010
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
3415
4011
  const originalTmux = process.env.TMUX;
@@ -3889,6 +4485,183 @@ exit 0
3889
4485
  }
3890
4486
  });
3891
4487
 
4488
+ it("blocks implementation file edits while deep-interview remains active after a clarified answer", async () => {
4489
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-edit-block-"));
4490
+ try {
4491
+ const stateDir = join(cwd, ".omx", "state");
4492
+ const sessionDir = join(stateDir, "sessions", "sess-di-edit-block");
4493
+ await mkdir(sessionDir, { recursive: true });
4494
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-edit-block", cwd });
4495
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
4496
+ version: 1,
4497
+ active: true,
4498
+ skill: "deep-interview",
4499
+ phase: "planning",
4500
+ session_id: "sess-di-edit-block",
4501
+ thread_id: "thread-di-edit-block",
4502
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-edit-block", thread_id: "thread-di-edit-block" }],
4503
+ });
4504
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
4505
+ active: true,
4506
+ mode: "deep-interview",
4507
+ current_phase: "intent-first",
4508
+ session_id: "sess-di-edit-block",
4509
+ thread_id: "thread-di-edit-block",
4510
+ rounds: [{ answer: "Implement by editing src/hooks/keyword-detector.ts and add tests." }],
4511
+ });
4512
+
4513
+ const result = await dispatchCodexNativeHook(
4514
+ {
4515
+ hook_event_name: "PreToolUse",
4516
+ cwd,
4517
+ session_id: "sess-di-edit-block",
4518
+ thread_id: "thread-di-edit-block",
4519
+ tool_name: "Edit",
4520
+ tool_use_id: "tool-di-edit-block",
4521
+ tool_input: { file_path: "src/hooks/keyword-detector.ts", old_string: "a", new_string: "b" },
4522
+ },
4523
+ { cwd },
4524
+ );
4525
+
4526
+ assert.equal(result.omxEventName, "pre-tool-use");
4527
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
4528
+ assert.match(String((result.outputJson as { reason?: string } | null)?.reason ?? ""), /Deep-interview is active/);
4529
+ assert.match(JSON.stringify(result.outputJson), /requirements\/spec mode/);
4530
+ assert.match(JSON.stringify(result.outputJson), /\$ralplan/);
4531
+ } finally {
4532
+ await rm(cwd, { recursive: true, force: true });
4533
+ }
4534
+ });
4535
+
4536
+ it("allows deep-interview artifact and state writes while blocking implementation Bash writes", async () => {
4537
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-artifact-"));
4538
+ try {
4539
+ const stateDir = join(cwd, ".omx", "state");
4540
+ const sessionDir = join(stateDir, "sessions", "sess-di-artifact");
4541
+ await mkdir(sessionDir, { recursive: true });
4542
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-artifact", cwd });
4543
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
4544
+ version: 1,
4545
+ active: true,
4546
+ skill: "deep-interview",
4547
+ phase: "planning",
4548
+ session_id: "sess-di-artifact",
4549
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-artifact" }],
4550
+ });
4551
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
4552
+ active: true,
4553
+ mode: "deep-interview",
4554
+ current_phase: "intent-first",
4555
+ session_id: "sess-di-artifact",
4556
+ });
4557
+
4558
+ const allowedWrite = await dispatchCodexNativeHook(
4559
+ {
4560
+ hook_event_name: "PreToolUse",
4561
+ cwd,
4562
+ session_id: "sess-di-artifact",
4563
+ tool_name: "Write",
4564
+ tool_use_id: "tool-di-spec-write",
4565
+ tool_input: { file_path: ".omx/specs/deep-interview-demo.md", content: "# Spec" },
4566
+ },
4567
+ { cwd },
4568
+ );
4569
+ assert.equal(allowedWrite.outputJson, null);
4570
+
4571
+ const allowedBash = await dispatchCodexNativeHook(
4572
+ {
4573
+ hook_event_name: "PreToolUse",
4574
+ cwd,
4575
+ session_id: "sess-di-artifact",
4576
+ tool_name: "Bash",
4577
+ tool_use_id: "tool-di-context-bash",
4578
+ tool_input: { command: "cat > .omx/context/demo.md <<'EOF'\n# Context\nEOF" },
4579
+ },
4580
+ { cwd },
4581
+ );
4582
+ assert.equal(allowedBash.outputJson, null);
4583
+
4584
+ const allowedAppendBash = await dispatchCodexNativeHook(
4585
+ {
4586
+ hook_event_name: "PreToolUse",
4587
+ cwd,
4588
+ session_id: "sess-di-artifact",
4589
+ tool_name: "Bash",
4590
+ tool_use_id: "tool-di-context-append-bash",
4591
+ tool_input: { command: "echo more context >> .omx/context/demo.md" },
4592
+ },
4593
+ { cwd },
4594
+ );
4595
+ assert.equal(allowedAppendBash.outputJson, null);
4596
+
4597
+ const blockedBash = await dispatchCodexNativeHook(
4598
+ {
4599
+ hook_event_name: "PreToolUse",
4600
+ cwd,
4601
+ session_id: "sess-di-artifact",
4602
+ tool_name: "Bash",
4603
+ tool_use_id: "tool-di-src-bash",
4604
+ tool_input: { command: "cat > src/implementation.ts <<'EOF'\nexport const x = 1;\nEOF" },
4605
+ },
4606
+ { cwd },
4607
+ );
4608
+ assert.equal((blockedBash.outputJson as { decision?: string } | null)?.decision, "block");
4609
+ } finally {
4610
+ await rm(cwd, { recursive: true, force: true });
4611
+ }
4612
+ });
4613
+
4614
+ it("allows implementation tools after an explicit deep-interview handoff deactivates the mode", async () => {
4615
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-handoff-"));
4616
+ try {
4617
+ const stateDir = join(cwd, ".omx", "state");
4618
+ const sessionDir = join(stateDir, "sessions", "sess-di-handoff");
4619
+ await mkdir(sessionDir, { recursive: true });
4620
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
4621
+ version: 1,
4622
+ active: true,
4623
+ skill: "deep-interview",
4624
+ phase: "planning",
4625
+ session_id: "sess-di-handoff",
4626
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-handoff" }],
4627
+ });
4628
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
4629
+ active: true,
4630
+ mode: "deep-interview",
4631
+ current_phase: "intent-first",
4632
+ session_id: "sess-di-handoff",
4633
+ });
4634
+
4635
+ await dispatchCodexNativeHook(
4636
+ {
4637
+ hook_event_name: "UserPromptSubmit",
4638
+ cwd,
4639
+ session_id: "sess-di-handoff",
4640
+ prompt: "$ralph implement the clarified spec in src/implementation.ts",
4641
+ },
4642
+ { cwd },
4643
+ );
4644
+
4645
+ const result = await dispatchCodexNativeHook(
4646
+ {
4647
+ hook_event_name: "PreToolUse",
4648
+ cwd,
4649
+ session_id: "sess-di-handoff",
4650
+ tool_name: "Edit",
4651
+ tool_use_id: "tool-di-post-handoff-edit",
4652
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
4653
+ },
4654
+ { cwd },
4655
+ );
4656
+
4657
+ assert.equal(result.outputJson, null);
4658
+ const completed = JSON.parse(await readFile(join(sessionDir, "deep-interview-state.json"), "utf-8")) as { active?: boolean };
4659
+ assert.equal(completed.active, false);
4660
+ } finally {
4661
+ await rm(cwd, { recursive: true, force: true });
4662
+ }
4663
+ });
4664
+
3892
4665
  it("returns a destructive-command caution on PreToolUse for rm -rf dist", async () => {
3893
4666
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-danger-"));
3894
4667
  try {
@@ -6581,6 +7354,7 @@ exit 0
6581
7354
  active: true,
6582
7355
  current_phase: "team-exec",
6583
7356
  team_name: "review-team",
7357
+ session_id: "sess-stop-team",
6584
7358
  });
6585
7359
  await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
6586
7360
  current_phase: "team-verify",
@@ -7468,73 +8242,157 @@ exit 0
7468
8242
  }
7469
8243
  });
7470
8244
 
7471
- it("emits one concise final decision summary and auto-finalize guidance when release-readiness already has a stable final recommendation and no active worker tasks", async () => {
7472
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-release-readiness-finalize-"));
8245
+ it("does not block Stop from canonical team state owned by another thread", async () => {
8246
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-canonical-other-thread-"));
7473
8247
  try {
7474
8248
  await initTeamState(
7475
- "release-ready-team",
7476
- "release readiness finalize",
8249
+ "canonical-other-thread-team",
8250
+ "canonical other-thread stop fallback",
7477
8251
  "executor",
7478
8252
  1,
7479
8253
  cwd,
7480
8254
  undefined,
7481
- { ...process.env, OMX_SESSION_ID: "sess-stop-release-ready" },
7482
- );
7483
- await writeReleaseReadinessLeaderAttention(
7484
- "release-ready-team",
7485
- "sess-stop-release-ready",
7486
- cwd,
7487
- { workRemaining: false },
7488
- );
7489
- await writeReleaseReadinessStateMarker(
7490
- "sess-stop-release-ready",
7491
- "release-ready-team",
7492
- cwd,
8255
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-canonical-thread" },
7493
8256
  );
8257
+ const manifestPath = join(cwd, ".omx", "state", "team", "canonical-other-thread-team", "manifest.v2.json");
8258
+ const manifest = JSON.parse(await readFile(manifestPath, "utf-8")) as Record<string, unknown>;
8259
+ await writeJson(manifestPath, {
8260
+ ...manifest,
8261
+ leader: {
8262
+ ...(manifest.leader as Record<string, unknown> | undefined),
8263
+ thread_id: "thread-other",
8264
+ },
8265
+ });
7494
8266
 
7495
8267
  const result = await dispatchCodexNativeHook(
7496
8268
  {
7497
8269
  hook_event_name: "Stop",
7498
8270
  cwd,
7499
- session_id: "sess-stop-release-ready",
7500
- thread_id: "thread-stop-release-ready",
7501
- turn_id: "turn-stop-release-ready-1",
7502
- mode: "release-readiness",
7503
- last_assistant_message: "Launch-ready: yes",
8271
+ session_id: "sess-stop-team-canonical-thread",
8272
+ thread_id: "thread-current",
7504
8273
  },
7505
8274
  { cwd },
7506
8275
  );
7507
8276
 
7508
8277
  assert.equal(result.omxEventName, "stop");
7509
- assert.deepEqual(result.outputJson, {
7510
- decision: "block",
7511
- reason:
7512
- 'Stable final recommendation already reached with no active worker tasks. Emit exactly one concise final decision summary aligned to "Launch-ready: yes." with no filler or residual acknowledgements (for example "yes"), then stop.',
7513
- stopReason: "release_readiness_auto_finalize",
7514
- systemMessage:
7515
- "OMX release-readiness detected a stable final recommendation with no active worker tasks; emit one concise final decision summary and finalize.",
7516
- });
8278
+ assert.equal(result.outputJson, null);
7517
8279
  } finally {
7518
8280
  await rm(cwd, { recursive: true, force: true });
7519
8281
  }
7520
8282
  });
7521
8283
 
7522
- it("does not auto-finalize non-release team stops that happen to contain a stable recommendation summary", async () => {
7523
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-non-release-readiness-control-"));
8284
+ it("blocks Stop from canonical team state owned by the current thread", async () => {
8285
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-canonical-current-thread-"));
7524
8286
  try {
7525
8287
  await initTeamState(
7526
- "general-review-team",
7527
- "general team stop control",
8288
+ "canonical-current-thread-team",
8289
+ "canonical current-thread stop fallback",
7528
8290
  "executor",
7529
8291
  1,
7530
8292
  cwd,
7531
8293
  undefined,
7532
- { ...process.env, OMX_SESSION_ID: "sess-stop-general-review" },
8294
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-canonical-current-thread" },
7533
8295
  );
7534
- await writeReleaseReadinessLeaderAttention(
7535
- "general-review-team",
7536
- "sess-stop-general-review",
7537
- cwd,
8296
+ const manifestPath = join(cwd, ".omx", "state", "team", "canonical-current-thread-team", "manifest.v2.json");
8297
+ const manifest = JSON.parse(await readFile(manifestPath, "utf-8")) as Record<string, unknown>;
8298
+ await writeJson(manifestPath, {
8299
+ ...manifest,
8300
+ leader: {
8301
+ ...(manifest.leader as Record<string, unknown> | undefined),
8302
+ thread_id: "thread-current",
8303
+ },
8304
+ });
8305
+
8306
+ const result = await dispatchCodexNativeHook(
8307
+ {
8308
+ hook_event_name: "Stop",
8309
+ cwd,
8310
+ session_id: "sess-stop-team-canonical-current-thread",
8311
+ thread_id: "thread-current",
8312
+ },
8313
+ { cwd },
8314
+ );
8315
+
8316
+ assert.equal(result.omxEventName, "stop");
8317
+ assert.deepEqual(result.outputJson, {
8318
+ decision: "block",
8319
+ reason:
8320
+ `OMX team pipeline is still active (canonical-current-thread-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
8321
+ stopReason: "team_team-exec",
8322
+ systemMessage: "OMX team pipeline is still active at phase team-exec.",
8323
+ });
8324
+ } finally {
8325
+ await rm(cwd, { recursive: true, force: true });
8326
+ }
8327
+ });
8328
+
8329
+ it("emits one concise final decision summary and auto-finalize guidance when release-readiness already has a stable final recommendation and no active worker tasks", async () => {
8330
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-release-readiness-finalize-"));
8331
+ try {
8332
+ await initTeamState(
8333
+ "release-ready-team",
8334
+ "release readiness finalize",
8335
+ "executor",
8336
+ 1,
8337
+ cwd,
8338
+ undefined,
8339
+ { ...process.env, OMX_SESSION_ID: "sess-stop-release-ready" },
8340
+ );
8341
+ await writeReleaseReadinessLeaderAttention(
8342
+ "release-ready-team",
8343
+ "sess-stop-release-ready",
8344
+ cwd,
8345
+ { workRemaining: false },
8346
+ );
8347
+ await writeReleaseReadinessStateMarker(
8348
+ "sess-stop-release-ready",
8349
+ "release-ready-team",
8350
+ cwd,
8351
+ );
8352
+
8353
+ const result = await dispatchCodexNativeHook(
8354
+ {
8355
+ hook_event_name: "Stop",
8356
+ cwd,
8357
+ session_id: "sess-stop-release-ready",
8358
+ thread_id: "thread-stop-release-ready",
8359
+ turn_id: "turn-stop-release-ready-1",
8360
+ mode: "release-readiness",
8361
+ last_assistant_message: "Launch-ready: yes",
8362
+ },
8363
+ { cwd },
8364
+ );
8365
+
8366
+ assert.equal(result.omxEventName, "stop");
8367
+ assert.deepEqual(result.outputJson, {
8368
+ decision: "block",
8369
+ reason:
8370
+ 'Stable final recommendation already reached with no active worker tasks. Emit exactly one concise final decision summary aligned to "Launch-ready: yes." with no filler or residual acknowledgements (for example "yes"), then stop.',
8371
+ stopReason: "release_readiness_auto_finalize",
8372
+ systemMessage:
8373
+ "OMX release-readiness detected a stable final recommendation with no active worker tasks; emit one concise final decision summary and finalize.",
8374
+ });
8375
+ } finally {
8376
+ await rm(cwd, { recursive: true, force: true });
8377
+ }
8378
+ });
8379
+
8380
+ it("does not auto-finalize non-release team stops that happen to contain a stable recommendation summary", async () => {
8381
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-non-release-readiness-control-"));
8382
+ try {
8383
+ await initTeamState(
8384
+ "general-review-team",
8385
+ "general team stop control",
8386
+ "executor",
8387
+ 1,
8388
+ cwd,
8389
+ undefined,
8390
+ { ...process.env, OMX_SESSION_ID: "sess-stop-general-review" },
8391
+ );
8392
+ await writeReleaseReadinessLeaderAttention(
8393
+ "general-review-team",
8394
+ "sess-stop-general-review",
8395
+ cwd,
7538
8396
  { workRemaining: false },
7539
8397
  );
7540
8398
 
@@ -7742,7 +8600,7 @@ exit 0
7742
8600
  const sharedRoot = join(cwd, "shared-root");
7743
8601
  const priorTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
7744
8602
  try {
7745
- process.env.OMX_TEAM_STATE_ROOT = "shared-root";
8603
+ process.env.OMX_TEAM_STATE_ROOT = sharedRoot;
7746
8604
  await initTeamState(
7747
8605
  "canonical-root-team",
7748
8606
  "canonical stop root fallback",
@@ -7750,7 +8608,7 @@ exit 0
7750
8608
  1,
7751
8609
  cwd,
7752
8610
  undefined,
7753
- { ...process.env, OMX_SESSION_ID: "sess-stop-team-root", OMX_TEAM_STATE_ROOT: "shared-root" },
8611
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-root", OMX_TEAM_STATE_ROOT: sharedRoot },
7754
8612
  );
7755
8613
 
7756
8614
  const result = await dispatchCodexNativeHook(
@@ -7815,9 +8673,10 @@ exit 0
7815
8673
 
7816
8674
  it("returns Stop continuation output from canonical team state rooted via OMX_TEAM_STATE_ROOT", async () => {
7817
8675
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-env-root-"));
8676
+ const teamStateRoot = join(cwd, "shared-team-state");
7818
8677
  const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
7819
8678
  try {
7820
- process.env.OMX_TEAM_STATE_ROOT = "shared-team-state";
8679
+ process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
7821
8680
  await initTeamState(
7822
8681
  "env-root-team",
7823
8682
  "env root stop fallback",
@@ -7828,7 +8687,7 @@ exit 0
7828
8687
  {
7829
8688
  ...process.env,
7830
8689
  OMX_SESSION_ID: "sess-stop-team-env-root",
7831
- OMX_TEAM_STATE_ROOT: "shared-team-state",
8690
+ OMX_TEAM_STATE_ROOT: teamStateRoot,
7832
8691
  },
7833
8692
  );
7834
8693
 
@@ -11052,6 +11911,8 @@ exit 0
11052
11911
  active: true,
11053
11912
  current_phase: "team-exec",
11054
11913
  team_name: "review-team",
11914
+ session_id: "sess-stop-team-refire",
11915
+ thread_id: "thread-stop-team-refire",
11055
11916
  });
11056
11917
  await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
11057
11918
  current_phase: "team-verify",
@@ -11291,6 +12152,250 @@ exit 0
11291
12152
  }
11292
12153
  });
11293
12154
 
12155
+ it("does not block Stop from root team state without team_name when no session is known", async () => {
12156
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-no-session-no-name-"));
12157
+ try {
12158
+ const stateDir = join(cwd, ".omx", "state");
12159
+ await mkdir(stateDir, { recursive: true });
12160
+ await writeJson(join(stateDir, "team-state.json"), {
12161
+ active: true,
12162
+ mode: "team",
12163
+ current_phase: "starting",
12164
+ });
12165
+
12166
+ const result = await dispatchCodexNativeHook(
12167
+ {
12168
+ hook_event_name: "Stop",
12169
+ cwd,
12170
+ },
12171
+ { cwd },
12172
+ );
12173
+
12174
+ assert.equal(result.omxEventName, "stop");
12175
+ assert.equal(result.outputJson, null);
12176
+ } finally {
12177
+ await rm(cwd, { recursive: true, force: true });
12178
+ }
12179
+ });
12180
+
12181
+ it("does not block Stop from root team state without team_name for a foreign session", async () => {
12182
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-foreign-no-name-"));
12183
+ try {
12184
+ const stateDir = join(cwd, ".omx", "state");
12185
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
12186
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
12187
+ await writeJson(join(stateDir, "team-state.json"), {
12188
+ active: true,
12189
+ mode: "team",
12190
+ current_phase: "starting",
12191
+ });
12192
+
12193
+ const result = await dispatchCodexNativeHook(
12194
+ {
12195
+ hook_event_name: "Stop",
12196
+ cwd,
12197
+ session_id: "sess-current",
12198
+ thread_id: "thread-current",
12199
+ },
12200
+ { cwd },
12201
+ );
12202
+
12203
+ assert.equal(result.omxEventName, "stop");
12204
+ assert.equal(result.outputJson, null);
12205
+ } finally {
12206
+ await rm(cwd, { recursive: true, force: true });
12207
+ }
12208
+ });
12209
+
12210
+ it("does not block Stop from another thread's stale root team state when no scoped team state exists", async () => {
12211
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-team-thread-"));
12212
+ try {
12213
+ const stateDir = join(cwd, ".omx", "state");
12214
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
12215
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
12216
+ await writeJson(join(stateDir, "team-state.json"), {
12217
+ active: true,
12218
+ current_phase: "starting",
12219
+ team_name: "stale-root-thread-team",
12220
+ session_id: "sess-current",
12221
+ thread_id: "thread-other",
12222
+ });
12223
+ await writeJson(join(stateDir, "team", "stale-root-thread-team", "phase.json"), {
12224
+ current_phase: "team-exec",
12225
+ max_fix_attempts: 3,
12226
+ current_fix_attempt: 0,
12227
+ transitions: [],
12228
+ updated_at: new Date().toISOString(),
12229
+ });
12230
+
12231
+ const result = await dispatchCodexNativeHook(
12232
+ {
12233
+ hook_event_name: "Stop",
12234
+ cwd,
12235
+ session_id: "sess-current",
12236
+ thread_id: "thread-current",
12237
+ },
12238
+ { cwd },
12239
+ );
12240
+
12241
+ assert.equal(result.omxEventName, "stop");
12242
+ assert.equal(result.outputJson, null);
12243
+ } finally {
12244
+ await rm(cwd, { recursive: true, force: true });
12245
+ }
12246
+ });
12247
+
12248
+ it("does not block Stop from root team state with matching session but missing thread ownership", async () => {
12249
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-missing-thread-"));
12250
+ try {
12251
+ const stateDir = join(cwd, ".omx", "state");
12252
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
12253
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
12254
+ await writeJson(join(stateDir, "team-state.json"), {
12255
+ active: true,
12256
+ current_phase: "starting",
12257
+ team_name: "root-missing-thread-team",
12258
+ session_id: "sess-current",
12259
+ });
12260
+ await writeJson(join(stateDir, "team", "root-missing-thread-team", "phase.json"), {
12261
+ current_phase: "team-exec",
12262
+ max_fix_attempts: 3,
12263
+ current_fix_attempt: 0,
12264
+ transitions: [],
12265
+ updated_at: new Date().toISOString(),
12266
+ });
12267
+
12268
+ const result = await dispatchCodexNativeHook(
12269
+ {
12270
+ hook_event_name: "Stop",
12271
+ cwd,
12272
+ session_id: "sess-current",
12273
+ thread_id: "thread-current",
12274
+ },
12275
+ { cwd },
12276
+ );
12277
+
12278
+ assert.equal(result.omxEventName, "stop");
12279
+ assert.equal(result.outputJson, null);
12280
+ } finally {
12281
+ await rm(cwd, { recursive: true, force: true });
12282
+ }
12283
+ });
12284
+
12285
+ it("does not block Stop from root team state when canonical phase is missing", async () => {
12286
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-missing-phase-"));
12287
+ try {
12288
+ const stateDir = join(cwd, ".omx", "state");
12289
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
12290
+ await mkdir(join(stateDir, "team", "root-missing-phase-team"), { recursive: true });
12291
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
12292
+ await writeJson(join(stateDir, "team-state.json"), {
12293
+ active: true,
12294
+ current_phase: "starting",
12295
+ team_name: "root-missing-phase-team",
12296
+ session_id: "sess-current",
12297
+ thread_id: "thread-current",
12298
+ });
12299
+
12300
+ const result = await dispatchCodexNativeHook(
12301
+ {
12302
+ hook_event_name: "Stop",
12303
+ cwd,
12304
+ session_id: "sess-current",
12305
+ thread_id: "thread-current",
12306
+ },
12307
+ { cwd },
12308
+ );
12309
+
12310
+ assert.equal(result.omxEventName, "stop");
12311
+ assert.equal(result.outputJson, null);
12312
+ } finally {
12313
+ await rm(cwd, { recursive: true, force: true });
12314
+ }
12315
+ });
12316
+
12317
+ it("does not block Stop from session-scoped team state owned by another thread", async () => {
12318
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-scoped-team-other-thread-"));
12319
+ try {
12320
+ const stateDir = join(cwd, ".omx", "state");
12321
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
12322
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
12323
+ await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
12324
+ active: true,
12325
+ current_phase: "starting",
12326
+ team_name: "scoped-other-thread-team",
12327
+ session_id: "sess-current",
12328
+ thread_id: "thread-other",
12329
+ });
12330
+ await writeJson(join(stateDir, "team", "scoped-other-thread-team", "phase.json"), {
12331
+ current_phase: "team-exec",
12332
+ max_fix_attempts: 3,
12333
+ current_fix_attempt: 0,
12334
+ transitions: [],
12335
+ updated_at: new Date().toISOString(),
12336
+ });
12337
+
12338
+ const result = await dispatchCodexNativeHook(
12339
+ {
12340
+ hook_event_name: "Stop",
12341
+ cwd,
12342
+ session_id: "sess-current",
12343
+ thread_id: "thread-current",
12344
+ },
12345
+ { cwd },
12346
+ );
12347
+
12348
+ assert.equal(result.omxEventName, "stop");
12349
+ assert.equal(result.outputJson, null);
12350
+ } finally {
12351
+ await rm(cwd, { recursive: true, force: true });
12352
+ }
12353
+ });
12354
+
12355
+ it("blocks Stop from session-scoped team state owned by the current session and thread", async () => {
12356
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-scoped-team-current-thread-"));
12357
+ try {
12358
+ const stateDir = join(cwd, ".omx", "state");
12359
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
12360
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
12361
+ await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
12362
+ active: true,
12363
+ current_phase: "starting",
12364
+ team_name: "scoped-current-team",
12365
+ session_id: "sess-current",
12366
+ thread_id: "thread-current",
12367
+ });
12368
+ await writeJson(join(stateDir, "team", "scoped-current-team", "phase.json"), {
12369
+ current_phase: "team-exec",
12370
+ max_fix_attempts: 3,
12371
+ current_fix_attempt: 0,
12372
+ transitions: [],
12373
+ updated_at: new Date().toISOString(),
12374
+ });
12375
+
12376
+ const result = await dispatchCodexNativeHook(
12377
+ {
12378
+ hook_event_name: "Stop",
12379
+ cwd,
12380
+ session_id: "sess-current",
12381
+ thread_id: "thread-current",
12382
+ },
12383
+ { cwd },
12384
+ );
12385
+
12386
+ assert.equal(result.omxEventName, "stop");
12387
+ assert.deepEqual(result.outputJson, {
12388
+ decision: "block",
12389
+ reason:
12390
+ `OMX team pipeline is still active (scoped-current-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
12391
+ stopReason: "team_team-exec",
12392
+ systemMessage: "OMX team pipeline is still active at phase team-exec.",
12393
+ });
12394
+ } finally {
12395
+ await rm(cwd, { recursive: true, force: true });
12396
+ }
12397
+ });
12398
+
11294
12399
  it("does not block Stop from another session's stale root team state when no scoped team state exists", async () => {
11295
12400
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-team-"));
11296
12401
  try {
@@ -11504,6 +12609,91 @@ describe("codex native hook triage integration", () => {
11504
12609
  }
11505
12610
  });
11506
12611
 
12612
+
12613
+ it("does not activate workflow state for native subagent prompts even when canonical id is the child session", async () => {
12614
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-subagent-keyword-"));
12615
+ const boxedRoot = await mkdtemp(join(tmpdir(), "omx-native-subagent-keyword-boxed-"));
12616
+ const originalOmxRoot = process.env.OMX_ROOT;
12617
+ const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
12618
+ const originalTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
12619
+ try {
12620
+ process.env.OMX_ROOT = boxedRoot;
12621
+ delete process.env.OMX_STATE_ROOT;
12622
+ delete process.env.OMX_TEAM_STATE_ROOT;
12623
+ const boxedStateDir = getBaseStateDir(cwd);
12624
+ await mkdir(boxedStateDir, { recursive: true });
12625
+ await writeJson(join(boxedStateDir, "subagent-tracking.json"), {
12626
+ schemaVersion: 1,
12627
+ sessions: {
12628
+ "omx-parent-session": {
12629
+ session_id: "omx-parent-session",
12630
+ leader_thread_id: "parent-native-thread",
12631
+ updated_at: "2026-05-21T19:04:40.000Z",
12632
+ threads: {
12633
+ "parent-native-thread": {
12634
+ thread_id: "parent-native-thread",
12635
+ kind: "leader",
12636
+ first_seen_at: "2026-05-21T19:04:40.000Z",
12637
+ last_seen_at: "2026-05-21T19:04:40.000Z",
12638
+ turn_count: 1,
12639
+ },
12640
+ "child-native-session": {
12641
+ thread_id: "child-native-session",
12642
+ kind: "subagent",
12643
+ first_seen_at: "2026-05-21T19:04:41.000Z",
12644
+ last_seen_at: "2026-05-21T19:04:41.000Z",
12645
+ turn_count: 1,
12646
+ mode: "review",
12647
+ },
12648
+ },
12649
+ },
12650
+ },
12651
+ });
12652
+
12653
+ const result = await dispatchCodexNativeHook(
12654
+ {
12655
+ hook_event_name: "UserPromptSubmit",
12656
+ cwd,
12657
+ session_id: "child-native-session",
12658
+ thread_id: "child-native-session",
12659
+ turn_id: "turn-subagent-review",
12660
+ prompt: [
12661
+ "Read-only review only. Do not edit files. Do not inspect/mutate OMX state/hooks.",
12662
+ "Context: The user asked for $autopilot, and this subagent must only review the patch.",
12663
+ ].join("\n\n"),
12664
+ },
12665
+ { cwd },
12666
+ );
12667
+
12668
+ const additionalContext = String(
12669
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
12670
+ );
12671
+ assert.equal(additionalContext, "");
12672
+ assert.equal(
12673
+ existsSync(join(boxedStateDir, "sessions", "child-native-session", "skill-active-state.json")),
12674
+ false,
12675
+ );
12676
+ assert.equal(
12677
+ existsSync(join(boxedStateDir, "sessions", "child-native-session", "autopilot-state.json")),
12678
+ false,
12679
+ );
12680
+ assert.equal(
12681
+ existsSync(join(cwd, ".omx", "state", "subagent-tracking.json")),
12682
+ false,
12683
+ "subagent tracking must not leak into the source worktree when OMX_ROOT is boxed",
12684
+ );
12685
+ } finally {
12686
+ if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
12687
+ else process.env.OMX_ROOT = originalOmxRoot;
12688
+ if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
12689
+ else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
12690
+ if (originalTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
12691
+ else process.env.OMX_TEAM_STATE_ROOT = originalTeamStateRoot;
12692
+ await rm(cwd, { recursive: true, force: true });
12693
+ await rm(boxedRoot, { recursive: true, force: true });
12694
+ }
12695
+ });
12696
+
11507
12697
  it("does not inject triage advisory for autopilot keyword prompts", async () => {
11508
12698
  const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-autopilot-"));
11509
12699
  try {
@@ -11535,6 +12725,62 @@ describe("codex native hook triage integration", () => {
11535
12725
  }
11536
12726
  });
11537
12727
 
12728
+ it("makes autopilot keyword activation observable in state, HUD context, and prompt guidance", async () => {
12729
+ const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-observable-"));
12730
+ try {
12731
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
12732
+ await writeSessionStart(cwd, "sess-autopilot-observable");
12733
+
12734
+ const result = await dispatchCodexNativeHook(
12735
+ {
12736
+ hook_event_name: "UserPromptSubmit",
12737
+ cwd,
12738
+ session_id: "sess-autopilot-observable",
12739
+ thread_id: "thread-autopilot-observable",
12740
+ turn_id: "turn-autopilot-observable",
12741
+ prompt: "$autopilot implement issue #2430",
12742
+ },
12743
+ { cwd },
12744
+ );
12745
+
12746
+ assert.equal(result.skillState?.skill, "autopilot");
12747
+ assert.equal(result.skillState?.phase, "deep-interview");
12748
+ assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-observable/autopilot-state.json");
12749
+
12750
+ const additionalContext = String(
12751
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
12752
+ );
12753
+ assert.match(additionalContext, /detected workflow keyword "\$autopilot" -> autopilot/);
12754
+ assert.match(additionalContext, /\$deep-interview -> \$ralplan -> \$ultragoal \(\+ \$team if needed\) -> \$code-review -> \$ultraqa/);
12755
+ assert.match(additionalContext, /deep_interview_gate\.skip_reason/);
12756
+ assert.match(additionalContext, /Do not silently fall back to ordinary \$plan\/ralplan-only handling/);
12757
+ assert.match(additionalContext, /Codex goal-mode handoff guidance/);
12758
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
12759
+
12760
+ const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-observable", "autopilot-state.json");
12761
+ const modeState = JSON.parse(await readFile(statePath, "utf-8")) as {
12762
+ active: boolean;
12763
+ current_phase: string;
12764
+ state?: { phase_cycle?: string[]; deep_interview_gate?: { status?: string; skip_reason?: string | null } };
12765
+ };
12766
+ assert.equal(modeState.active, true);
12767
+ assert.equal(modeState.current_phase, "deep-interview");
12768
+ assert.deepEqual(modeState.state?.phase_cycle, ["deep-interview", "ralplan", "ultragoal", "code-review", "ultraqa"]);
12769
+ assert.deepEqual(modeState.state?.deep_interview_gate, {
12770
+ status: "required",
12771
+ skip_reason: null,
12772
+ rationale: "Autopilot starts at the deep-interview gate by default; clear bounded tasks may skip only with an explicit persisted skip reason.",
12773
+ });
12774
+
12775
+ const hudState = await readAllState(cwd);
12776
+ assert.equal(hudState.autopilot?.active, true);
12777
+ assert.equal(hudState.autopilot?.current_phase, "deep-interview");
12778
+ assert.match(renderHud(hudState, "focused"), /autopilot:deep-interview/);
12779
+ } finally {
12780
+ await rm(cwd, { recursive: true, force: true });
12781
+ }
12782
+ });
12783
+
11538
12784
  // ── Group 2: HEAVY injection ─────────────────────────────────────────────
11539
12785
 
11540
12786
  it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {