oh-my-codex 0.18.6 → 0.18.8

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 (444) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +59 -10
  4. package/crates/omx-sparkshell/tests/execution.rs +1 -1
  5. package/dist/agents/__tests__/definitions.test.js +11 -0
  6. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  7. package/dist/agents/__tests__/native-config.test.js +56 -6
  8. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  9. package/dist/agents/definitions.d.ts +10 -0
  10. package/dist/agents/definitions.d.ts.map +1 -1
  11. package/dist/agents/definitions.js +5 -1
  12. package/dist/agents/definitions.js.map +1 -1
  13. package/dist/agents/native-config.d.ts +5 -1
  14. package/dist/agents/native-config.d.ts.map +1 -1
  15. package/dist/agents/native-config.js +19 -4
  16. package/dist/agents/native-config.js.map +1 -1
  17. package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
  18. package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
  19. package/dist/autopilot/__tests__/fsm.test.js +75 -0
  20. package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
  21. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
  22. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
  23. package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
  24. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
  25. package/dist/autopilot/deep-interview-gate.d.ts +18 -0
  26. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
  27. package/dist/autopilot/deep-interview-gate.js +256 -0
  28. package/dist/autopilot/deep-interview-gate.js.map +1 -0
  29. package/dist/autopilot/fsm.d.ts +13 -0
  30. package/dist/autopilot/fsm.d.ts.map +1 -0
  31. package/dist/autopilot/fsm.js +70 -0
  32. package/dist/autopilot/fsm.js.map +1 -0
  33. package/dist/autopilot/ralplan-gate.d.ts +17 -0
  34. package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
  35. package/dist/autopilot/ralplan-gate.js +61 -0
  36. package/dist/autopilot/ralplan-gate.js.map +1 -0
  37. package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
  38. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  39. package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
  40. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  41. package/dist/cli/__tests__/index.test.js +83 -7
  42. package/dist/cli/__tests__/index.test.js.map +1 -1
  43. package/dist/cli/__tests__/launch-fallback.test.js +175 -6
  44. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  45. package/dist/cli/__tests__/package-bin-contract.test.js +8 -4
  46. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  47. package/dist/cli/__tests__/question.test.js +100 -0
  48. package/dist/cli/__tests__/question.test.js.map +1 -1
  49. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
  50. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  51. package/dist/cli/__tests__/ralph.test.js +14 -0
  52. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  53. package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
  54. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  55. package/dist/cli/__tests__/setup-refresh.test.js +83 -0
  56. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  57. package/dist/cli/__tests__/state.test.js +21 -0
  58. package/dist/cli/__tests__/state.test.js.map +1 -1
  59. package/dist/cli/__tests__/team.test.js +2 -2
  60. package/dist/cli/__tests__/team.test.js.map +1 -1
  61. package/dist/cli/__tests__/update.test.js +110 -2
  62. package/dist/cli/__tests__/update.test.js.map +1 -1
  63. package/dist/cli/doctor.d.ts.map +1 -1
  64. package/dist/cli/doctor.js +8 -1
  65. package/dist/cli/doctor.js.map +1 -1
  66. package/dist/cli/index.d.ts +14 -3
  67. package/dist/cli/index.d.ts.map +1 -1
  68. package/dist/cli/index.js +298 -50
  69. package/dist/cli/index.js.map +1 -1
  70. package/dist/cli/plugin-marketplace.d.ts +14 -2
  71. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  72. package/dist/cli/plugin-marketplace.js +62 -15
  73. package/dist/cli/plugin-marketplace.js.map +1 -1
  74. package/dist/cli/question.d.ts.map +1 -1
  75. package/dist/cli/question.js +36 -5
  76. package/dist/cli/question.js.map +1 -1
  77. package/dist/cli/ralph.d.ts.map +1 -1
  78. package/dist/cli/ralph.js +3 -1
  79. package/dist/cli/ralph.js.map +1 -1
  80. package/dist/cli/setup-preferences.d.ts +2 -0
  81. package/dist/cli/setup-preferences.d.ts.map +1 -1
  82. package/dist/cli/setup-preferences.js +4 -0
  83. package/dist/cli/setup-preferences.js.map +1 -1
  84. package/dist/cli/setup.d.ts +3 -0
  85. package/dist/cli/setup.d.ts.map +1 -1
  86. package/dist/cli/setup.js +166 -27
  87. package/dist/cli/setup.js.map +1 -1
  88. package/dist/cli/state.d.ts.map +1 -1
  89. package/dist/cli/state.js +8 -1
  90. package/dist/cli/state.js.map +1 -1
  91. package/dist/cli/tmux-hook.d.ts.map +1 -1
  92. package/dist/cli/tmux-hook.js +16 -0
  93. package/dist/cli/tmux-hook.js.map +1 -1
  94. package/dist/cli/update.d.ts +2 -0
  95. package/dist/cli/update.d.ts.map +1 -1
  96. package/dist/cli/update.js +47 -3
  97. package/dist/cli/update.js.map +1 -1
  98. package/dist/config/__tests__/deep-interview.test.js +7 -6
  99. package/dist/config/__tests__/deep-interview.test.js.map +1 -1
  100. package/dist/config/__tests__/generator-notify.test.js +1 -0
  101. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  102. package/dist/config/deep-interview.d.ts.map +1 -1
  103. package/dist/config/deep-interview.js +14 -4
  104. package/dist/config/deep-interview.js.map +1 -1
  105. package/dist/config/generator.d.ts +2 -2
  106. package/dist/config/generator.d.ts.map +1 -1
  107. package/dist/config/generator.js +2 -2
  108. package/dist/config/generator.js.map +1 -1
  109. package/dist/config/team-mode.d.ts +12 -0
  110. package/dist/config/team-mode.d.ts.map +1 -0
  111. package/dist/config/team-mode.js +91 -0
  112. package/dist/config/team-mode.js.map +1 -0
  113. package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
  114. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  115. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
  116. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  117. package/dist/hooks/__tests__/code-review-skill-contract.test.js +8 -0
  118. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  119. package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
  120. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  121. package/dist/hooks/__tests__/keyword-detector.test.js +1072 -14
  122. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  123. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +64 -1
  124. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  125. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
  126. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  127. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
  128. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  129. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
  130. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  131. package/dist/hooks/__tests__/session.test.js +25 -0
  132. package/dist/hooks/__tests__/session.test.js.map +1 -1
  133. package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
  134. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  135. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  136. package/dist/hooks/agents-overlay.js +36 -50
  137. package/dist/hooks/agents-overlay.js.map +1 -1
  138. package/dist/hooks/deep-interview-config-instruction.js +1 -1
  139. package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
  140. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
  141. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  142. package/dist/hooks/extensibility/plugin-runner.js +17 -21
  143. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  144. package/dist/hooks/keyword-detector.d.ts +1 -0
  145. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  146. package/dist/hooks/keyword-detector.js +428 -32
  147. package/dist/hooks/keyword-detector.js.map +1 -1
  148. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  149. package/dist/hooks/keyword-registry.js +1 -0
  150. package/dist/hooks/keyword-registry.js.map +1 -1
  151. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  152. package/dist/hooks/prompt-guidance-contract.js +6 -0
  153. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  154. package/dist/hooks/session.d.ts +3 -0
  155. package/dist/hooks/session.d.ts.map +1 -1
  156. package/dist/hooks/session.js +13 -5
  157. package/dist/hooks/session.js.map +1 -1
  158. package/dist/hud/__tests__/authority.test.js +469 -31
  159. package/dist/hud/__tests__/authority.test.js.map +1 -1
  160. package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
  161. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  162. package/dist/hud/__tests__/index.test.js +210 -2
  163. package/dist/hud/__tests__/index.test.js.map +1 -1
  164. package/dist/hud/__tests__/reconcile.test.js +588 -28
  165. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  166. package/dist/hud/__tests__/render.test.js +61 -0
  167. package/dist/hud/__tests__/render.test.js.map +1 -1
  168. package/dist/hud/__tests__/state.test.js +208 -0
  169. package/dist/hud/__tests__/state.test.js.map +1 -1
  170. package/dist/hud/__tests__/tmux.test.js +314 -22
  171. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  172. package/dist/hud/authority.d.ts +5 -0
  173. package/dist/hud/authority.d.ts.map +1 -1
  174. package/dist/hud/authority.js +337 -30
  175. package/dist/hud/authority.js.map +1 -1
  176. package/dist/hud/index.d.ts +20 -2
  177. package/dist/hud/index.d.ts.map +1 -1
  178. package/dist/hud/index.js +103 -26
  179. package/dist/hud/index.js.map +1 -1
  180. package/dist/hud/reconcile.d.ts +3 -3
  181. package/dist/hud/reconcile.d.ts.map +1 -1
  182. package/dist/hud/reconcile.js +129 -20
  183. package/dist/hud/reconcile.js.map +1 -1
  184. package/dist/hud/render.d.ts.map +1 -1
  185. package/dist/hud/render.js +35 -0
  186. package/dist/hud/render.js.map +1 -1
  187. package/dist/hud/state.d.ts.map +1 -1
  188. package/dist/hud/state.js +64 -50
  189. package/dist/hud/state.js.map +1 -1
  190. package/dist/hud/tmux.d.ts +26 -6
  191. package/dist/hud/tmux.d.ts.map +1 -1
  192. package/dist/hud/tmux.js +173 -38
  193. package/dist/hud/tmux.js.map +1 -1
  194. package/dist/hud/types.d.ts +11 -0
  195. package/dist/hud/types.d.ts.map +1 -1
  196. package/dist/hud/types.js.map +1 -1
  197. package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
  198. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  199. package/dist/mcp/__tests__/state-paths.test.js +71 -1
  200. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  201. package/dist/mcp/__tests__/state-server.test.js +13 -1
  202. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  203. package/dist/mcp/hermes-bridge.d.ts +12 -2
  204. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  205. package/dist/mcp/hermes-bridge.js +83 -9
  206. package/dist/mcp/hermes-bridge.js.map +1 -1
  207. package/dist/mcp/state-paths.d.ts +32 -0
  208. package/dist/mcp/state-paths.d.ts.map +1 -1
  209. package/dist/mcp/state-paths.js +113 -17
  210. package/dist/mcp/state-paths.js.map +1 -1
  211. package/dist/mcp/state-server.d.ts +4 -4
  212. package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
  213. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  214. package/dist/pipeline/__tests__/stages.test.js +130 -0
  215. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  216. package/dist/pipeline/orchestrator.js +1 -1
  217. package/dist/pipeline/orchestrator.js.map +1 -1
  218. package/dist/pipeline/stages/ralplan.d.ts +1 -0
  219. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  220. package/dist/pipeline/stages/ralplan.js +14 -5
  221. package/dist/pipeline/stages/ralplan.js.map +1 -1
  222. package/dist/question/__tests__/deep-interview.test.js +160 -2
  223. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  224. package/dist/question/__tests__/policy.test.js +63 -3
  225. package/dist/question/__tests__/policy.test.js.map +1 -1
  226. package/dist/question/__tests__/renderer.test.js +191 -2
  227. package/dist/question/__tests__/renderer.test.js.map +1 -1
  228. package/dist/question/__tests__/state.test.js +94 -3
  229. package/dist/question/__tests__/state.test.js.map +1 -1
  230. package/dist/question/__tests__/ui.test.js +4 -0
  231. package/dist/question/__tests__/ui.test.js.map +1 -1
  232. package/dist/question/autopilot-wait.d.ts +12 -2
  233. package/dist/question/autopilot-wait.d.ts.map +1 -1
  234. package/dist/question/autopilot-wait.js +158 -47
  235. package/dist/question/autopilot-wait.js.map +1 -1
  236. package/dist/question/deep-interview.d.ts.map +1 -1
  237. package/dist/question/deep-interview.js +22 -6
  238. package/dist/question/deep-interview.js.map +1 -1
  239. package/dist/question/policy.d.ts.map +1 -1
  240. package/dist/question/policy.js +2 -5
  241. package/dist/question/policy.js.map +1 -1
  242. package/dist/question/renderer.d.ts +12 -0
  243. package/dist/question/renderer.d.ts.map +1 -1
  244. package/dist/question/renderer.js +87 -3
  245. package/dist/question/renderer.js.map +1 -1
  246. package/dist/question/state.d.ts +8 -1
  247. package/dist/question/state.d.ts.map +1 -1
  248. package/dist/question/state.js +54 -14
  249. package/dist/question/state.js.map +1 -1
  250. package/dist/question/types.d.ts +1 -1
  251. package/dist/question/types.d.ts.map +1 -1
  252. package/dist/question/ui.d.ts +1 -0
  253. package/dist/question/ui.d.ts.map +1 -1
  254. package/dist/question/ui.js +1 -0
  255. package/dist/question/ui.js.map +1 -1
  256. package/dist/ralplan/__tests__/runtime.test.js +191 -0
  257. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  258. package/dist/ralplan/consensus-gate.d.ts +9 -1
  259. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  260. package/dist/ralplan/consensus-gate.js +84 -2
  261. package/dist/ralplan/consensus-gate.js.map +1 -1
  262. package/dist/ralplan/runtime.d.ts +9 -0
  263. package/dist/ralplan/runtime.d.ts.map +1 -1
  264. package/dist/ralplan/runtime.js +32 -11
  265. package/dist/ralplan/runtime.js.map +1 -1
  266. package/dist/scripts/__tests__/codex-native-hook.test.js +2315 -280
  267. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  268. package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
  269. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  270. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
  271. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
  272. package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
  273. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
  274. package/dist/scripts/__tests__/run-test-files.test.js +74 -0
  275. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  276. package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
  277. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  278. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  279. package/dist/scripts/codex-native-hook.js +431 -56
  280. package/dist/scripts/codex-native-hook.js.map +1 -1
  281. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  282. package/dist/scripts/codex-native-pre-post.js +79 -1
  283. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  284. package/dist/scripts/eval/eval-parity-smoke.js +1 -1
  285. package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
  286. package/dist/scripts/hook-payload-guard.d.ts +9 -0
  287. package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
  288. package/dist/scripts/hook-payload-guard.js +111 -0
  289. package/dist/scripts/hook-payload-guard.js.map +1 -0
  290. package/dist/scripts/notify-fallback-watcher.js +8 -1
  291. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  292. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
  293. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
  294. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
  295. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
  296. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  297. package/dist/scripts/notify-hook/auto-nudge.js +3 -1
  298. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  299. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  300. package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
  301. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  302. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  303. package/dist/scripts/notify-hook/state-io.js +62 -38
  304. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  305. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  306. package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
  307. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  308. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  309. package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
  310. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  311. package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
  312. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  313. package/dist/scripts/notify-hook/tmux-injection.js +24 -18
  314. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  315. package/dist/scripts/notify-hook.js +86 -13
  316. package/dist/scripts/notify-hook.js.map +1 -1
  317. package/dist/scripts/run-test-files.js +193 -22
  318. package/dist/scripts/run-test-files.js.map +1 -1
  319. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  320. package/dist/scripts/sync-plugin-mirror.js +61 -3
  321. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  322. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  323. package/dist/scripts/verify-native-agents.js +58 -1
  324. package/dist/scripts/verify-native-agents.js.map +1 -1
  325. package/dist/state/__tests__/operations.test.js +1125 -1
  326. package/dist/state/__tests__/operations.test.js.map +1 -1
  327. package/dist/state/__tests__/skill-active.test.js +46 -1
  328. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  329. package/dist/state/__tests__/workflow-transition.test.js +98 -7
  330. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  331. package/dist/state/operations.d.ts.map +1 -1
  332. package/dist/state/operations.js +159 -2
  333. package/dist/state/operations.js.map +1 -1
  334. package/dist/state/skill-active.js +6 -8
  335. package/dist/state/skill-active.js.map +1 -1
  336. package/dist/state/workflow-transition-reconcile.d.ts +6 -0
  337. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  338. package/dist/state/workflow-transition-reconcile.js +38 -15
  339. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  340. package/dist/state/workflow-transition.d.ts.map +1 -1
  341. package/dist/state/workflow-transition.js +10 -3
  342. package/dist/state/workflow-transition.js.map +1 -1
  343. package/dist/subagents/__tests__/tracker.test.js +139 -0
  344. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  345. package/dist/subagents/tracker.d.ts +3 -0
  346. package/dist/subagents/tracker.d.ts.map +1 -1
  347. package/dist/subagents/tracker.js +41 -4
  348. package/dist/subagents/tracker.js.map +1 -1
  349. package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
  350. package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
  351. package/dist/team/__tests__/coordination-protocol.test.js +173 -0
  352. package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
  353. package/dist/team/__tests__/runtime.test.js +52 -3
  354. package/dist/team/__tests__/runtime.test.js.map +1 -1
  355. package/dist/team/__tests__/scaling.test.js +9 -4
  356. package/dist/team/__tests__/scaling.test.js.map +1 -1
  357. package/dist/team/__tests__/state.test.js +83 -0
  358. package/dist/team/__tests__/state.test.js.map +1 -1
  359. package/dist/team/__tests__/tmux-session.test.js +240 -2
  360. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  361. package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
  362. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  363. package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
  364. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  365. package/dist/team/coordination-protocol.d.ts +14 -0
  366. package/dist/team/coordination-protocol.d.ts.map +1 -0
  367. package/dist/team/coordination-protocol.js +244 -0
  368. package/dist/team/coordination-protocol.js.map +1 -0
  369. package/dist/team/runtime.d.ts +1 -0
  370. package/dist/team/runtime.d.ts.map +1 -1
  371. package/dist/team/runtime.js +19 -3
  372. package/dist/team/runtime.js.map +1 -1
  373. package/dist/team/scaling.d.ts.map +1 -1
  374. package/dist/team/scaling.js +3 -2
  375. package/dist/team/scaling.js.map +1 -1
  376. package/dist/team/state/tasks.d.ts.map +1 -1
  377. package/dist/team/state/tasks.js +24 -0
  378. package/dist/team/state/tasks.js.map +1 -1
  379. package/dist/team/state/types.d.ts +21 -1
  380. package/dist/team/state/types.d.ts.map +1 -1
  381. package/dist/team/state/types.js.map +1 -1
  382. package/dist/team/state.d.ts +17 -1
  383. package/dist/team/state.d.ts.map +1 -1
  384. package/dist/team/state.js +12 -5
  385. package/dist/team/state.js.map +1 -1
  386. package/dist/team/team-ops.d.ts +1 -1
  387. package/dist/team/team-ops.d.ts.map +1 -1
  388. package/dist/team/team-ops.js.map +1 -1
  389. package/dist/team/tmux-session.d.ts +2 -0
  390. package/dist/team/tmux-session.d.ts.map +1 -1
  391. package/dist/team/tmux-session.js +161 -13
  392. package/dist/team/tmux-session.js.map +1 -1
  393. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  394. package/dist/team/worker-bootstrap.js +63 -0
  395. package/dist/team/worker-bootstrap.js.map +1 -1
  396. package/dist/utils/__tests__/agents-model-table.test.js +4 -2
  397. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  398. package/dist/utils/agents-model-table.d.ts.map +1 -1
  399. package/dist/utils/agents-model-table.js +3 -0
  400. package/dist/utils/agents-model-table.js.map +1 -1
  401. package/dist/verification/__tests__/ci-rust-gates.test.js +81 -1
  402. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  403. package/package.json +8 -8
  404. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  405. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
  406. package/plugins/oh-my-codex/hooks/hooks.json +1 -2
  407. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -6
  408. package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
  409. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
  410. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
  411. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
  412. package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
  413. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
  414. package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
  415. package/skills/autopilot/SKILL.md +13 -6
  416. package/skills/code-review/SKILL.md +7 -7
  417. package/skills/deep-interview/SKILL.md +9 -4
  418. package/skills/ralph/SKILL.md +22 -22
  419. package/skills/ralplan/SKILL.md +12 -0
  420. package/skills/team/SKILL.md +16 -0
  421. package/skills/ultraqa/SKILL.md +9 -0
  422. package/skills/worker/SKILL.md +14 -0
  423. package/src/scripts/__tests__/codex-native-hook.test.ts +4435 -2083
  424. package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
  425. package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
  426. package/src/scripts/__tests__/run-test-files.test.ts +102 -0
  427. package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
  428. package/src/scripts/codex-native-hook.ts +536 -51
  429. package/src/scripts/codex-native-pre-post.ts +80 -0
  430. package/src/scripts/demo-team-e2e.sh +10 -7
  431. package/src/scripts/eval/eval-parity-smoke.ts +1 -1
  432. package/src/scripts/hook-payload-guard.ts +113 -0
  433. package/src/scripts/notify-fallback-watcher.ts +8 -1
  434. package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
  435. package/src/scripts/notify-hook/auto-nudge.ts +3 -1
  436. package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
  437. package/src/scripts/notify-hook/state-io.ts +75 -37
  438. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
  439. package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
  440. package/src/scripts/notify-hook/tmux-injection.ts +35 -19
  441. package/src/scripts/notify-hook.ts +105 -6
  442. package/src/scripts/run-test-files.ts +192 -22
  443. package/src/scripts/sync-plugin-mirror.ts +98 -9
  444. package/src/scripts/verify-native-agents.ts +65 -1
@@ -22,6 +22,8 @@ import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.j
22
22
  import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
23
23
  import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
24
24
  import { getBaseStateDir } from "../../state/paths.js";
25
+ import { maybeNudgeLeaderForAllowedWorkerStop } from "../notify-hook/team-worker-stop.js";
26
+ import { MAX_NATIVE_STDIN_JSON_BYTES } from "../hook-payload-guard.js";
25
27
  function nativeHookScriptPath() {
26
28
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
27
29
  }
@@ -44,6 +46,23 @@ async function writeJson(path, value) {
44
46
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
45
47
  await writeFile(path, JSON.stringify(value, null, 2));
46
48
  }
49
+ async function writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId) {
50
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
51
+ await writeJson(join(stateDir, "session.json"), {
52
+ session_id: sessionId,
53
+ native_session_id: nativeSessionId,
54
+ cwd,
55
+ });
56
+ }
57
+ async function writeSessionSkillActiveState(stateDir, sessionId, skill, phase) {
58
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
59
+ active: true,
60
+ skill,
61
+ phase,
62
+ session_id: sessionId,
63
+ active_skills: [{ skill, phase, active: true, session_id: sessionId }],
64
+ });
65
+ }
47
66
  async function setTeamPaneIds(cwd, teamName, paneIds) {
48
67
  for (const fileName of ["config.json", "manifest.v2.json"]) {
49
68
  const filePath = join(cwd, ".omx", "state", "team", teamName, fileName);
@@ -104,6 +123,12 @@ async function withLoreGuardConfig(value, prefix, run) {
104
123
  }
105
124
  }
106
125
  function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
126
+ const rawCaptureText = options.captureText ?? (options.busyLeader ? "• Working… (esc to interrupt)" : "› ready");
127
+ const captureText = `'${rawCaptureText.replace(/'/g, "'\"'\"'")}'`;
128
+ const currentCommand = `'${(options.currentCommand ?? "codex").replace(/'/g, "'\"'\"'")}'`;
129
+ const sendDelaySeconds = Math.max(0, options.sendDelayMs ?? 0) / 1000;
130
+ const removePathOnSend = options.removePathOnSend ? `'${options.removePathOnSend.replace(/'/g, "'\"'\"'")}'` : "";
131
+ const removePathOnCapture = options.removePathOnCapture ? `'${options.removePathOnCapture.replace(/'/g, "'\"'\"'")}'` : "";
107
132
  return `#!/usr/bin/env bash
108
133
  set -eu
109
134
  echo "$@" >> "${tmuxLogPath}"
@@ -124,17 +149,20 @@ if [[ "$cmd" == "display-message" ]]; then
124
149
  "#{pane_id}") echo "%42" ;;
125
150
  "#{pane_current_path}") pwd ;;
126
151
  "#{pane_start_command}") echo "codex" ;;
127
- "#{pane_current_command}") echo "codex" ;;
152
+ "#{pane_current_command}") printf '%s\\n' ${currentCommand} ;;
128
153
  "#S") echo "omx-team-worker-stop" ;;
129
154
  *) ;;
130
155
  esac
131
156
  exit 0
132
157
  fi
133
158
  if [[ "$cmd" == "capture-pane" ]]; then
134
- ${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
159
+ ${removePathOnCapture ? `rm -rf ${removePathOnCapture}` : ""}
160
+ printf '%s\\n' ${captureText}
135
161
  exit 0
136
162
  fi
137
163
  if [[ "$cmd" == "send-keys" ]]; then
164
+ ${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
165
+ ${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
138
166
  ${options.failSend ? "exit 1" : "exit 0"}
139
167
  fi
140
168
  exit 0
@@ -281,13 +309,78 @@ describe("codex native hook dispatch", () => {
281
309
  it("does not treat a different module url as the main module", () => {
282
310
  assert.equal(isCodexNativeHookMainModule(pathToFileURL("/tmp/omx native/other-script.js").href, "/tmp/omx native/codex-native-hook.js"), false);
283
311
  });
284
- it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
312
+ it("emits schema-safe JSON stdout when CLI stdin is malformed", () => {
285
313
  const stdout = runNativeHookCli("{");
286
314
  const output = parseSingleJsonStdout(stdout);
315
+ assert.equal(output.continue, false);
316
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
317
+ assert.equal(output.hookSpecificOutput, undefined);
318
+ assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
319
+ });
320
+ it("redacts unterminated prompt-like malformed stdin fields", async () => {
321
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-unterminated-"));
322
+ try {
323
+ const privatePrompt = "PRIVATE_UNTERMINATED_PROMPT";
324
+ const malformed = `{hook_event_name:"PostToolUse", prompt:"${privatePrompt}`;
325
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
326
+ cwd,
327
+ input: malformed,
328
+ encoding: "utf-8",
329
+ stdio: ["pipe", "pipe", "pipe"],
330
+ });
331
+ assert.equal(result.status, 0, result.stderr || result.stdout);
332
+ assert.equal(result.stderr, "");
333
+ const output = parseSingleJsonStdout(result.stdout);
334
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
335
+ const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
336
+ const entry = JSON.parse(log.trim());
337
+ const prefix = String(entry.raw_input_prefix ?? "");
338
+ assert.doesNotMatch(prefix, new RegExp(privatePrompt));
339
+ assert.match(prefix, /prompt:"\[REDACTED\]"/);
340
+ }
341
+ finally {
342
+ await rm(cwd, { recursive: true, force: true });
343
+ }
344
+ });
345
+ it("logs a bounded redacted raw stdin prefix when CLI stdin is malformed", async () => {
346
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-log-prefix-"));
347
+ try {
348
+ const secret = "sk-test-secret123456";
349
+ const promptText = "summarize private launch notes";
350
+ const malformed = `{hook_event_name:"PostToolUse", access_token:"${secret}", prompt:"${promptText}", text:"${promptText}", bad:"${"x".repeat(400)}"}${String.fromCharCode(10, 0, 7)}`;
351
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
352
+ cwd,
353
+ input: malformed,
354
+ encoding: "utf-8",
355
+ stdio: ["pipe", "pipe", "pipe"],
356
+ });
357
+ assert.equal(result.status, 0, result.stderr || result.stdout);
358
+ assert.equal(result.stderr, "");
359
+ const output = parseSingleJsonStdout(result.stdout);
360
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
361
+ const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
362
+ const entry = JSON.parse(log.trim());
363
+ const prefix = String(entry.raw_input_prefix ?? "");
364
+ assert.equal(entry.type, "native_hook_stdin_parse_error");
365
+ assert.equal(entry.raw_input_length, Buffer.byteLength(malformed, "utf-8"));
366
+ assert.ok(prefix.length <= 240, `prefix should be bounded, got ${prefix.length}`);
367
+ assert.doesNotMatch(prefix, /[\u0000-\u001f\u007f-\u009f]/);
368
+ assert.doesNotMatch(prefix, new RegExp(secret));
369
+ assert.doesNotMatch(prefix, new RegExp(promptText));
370
+ assert.match(prefix, /\[REDACTED\]/);
371
+ }
372
+ finally {
373
+ await rm(cwd, { recursive: true, force: true });
374
+ }
375
+ });
376
+ it("emits Stop-schema-safe block JSON when malformed stdin still identifies Stop", () => {
377
+ const stdout = runNativeHookCli('{hook_event_name:"Stop",');
378
+ const output = parseSingleJsonStdout(stdout);
287
379
  assert.equal(output.decision, "block");
288
380
  assert.equal(output.reason, "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.");
289
- assert.equal(output.hookSpecificOutput?.hookEventName, "Unknown");
290
- assert.match(String(output.hookSpecificOutput?.additionalContext ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
381
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
382
+ assert.equal(output.hookSpecificOutput, undefined);
383
+ assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
291
384
  });
292
385
  it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
293
386
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
@@ -301,6 +394,112 @@ describe("codex native hook dispatch", () => {
301
394
  }, { cwd });
302
395
  const output = parseSingleJsonStdout(stdout);
303
396
  assert.deepEqual(output, {});
397
+ assert.equal(existsSync(join(cwd, ".omx", "state")), false);
398
+ }
399
+ finally {
400
+ await rm(cwd, { recursive: true, force: true });
401
+ }
402
+ });
403
+ it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
404
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
405
+ try {
406
+ const oversizedStop = JSON.stringify({
407
+ hook_event_name: "Stop",
408
+ cwd,
409
+ session_id: "sess-cli-stop-oversized",
410
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
411
+ });
412
+ const stdout = runNativeHookCli(oversizedStop, { cwd });
413
+ assert.deepEqual(parseSingleJsonStdout(stdout), {});
414
+ assert.equal(existsSync(join(cwd, ".omx", "state")), false);
415
+ }
416
+ finally {
417
+ await rm(cwd, { recursive: true, force: true });
418
+ }
419
+ });
420
+ it("blocks oversized Stop stdin when current session autopilot is active", async () => {
421
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-active-"));
422
+ try {
423
+ await writeActiveAutopilotSession(cwd, "sess-cli-stop-oversized-active");
424
+ const oversizedStop = JSON.stringify({
425
+ hook_event_name: "Stop",
426
+ cwd,
427
+ session_id: "native-session-hidden-by-oversized-payload",
428
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
429
+ });
430
+ const output = parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd }));
431
+ assert.equal(output.decision, "block");
432
+ assert.equal(output.stopReason, "native_stop_stdin_oversized_active_workflow");
433
+ assert.match(String(output.systemMessage ?? ""), /active current-session workflow state/);
434
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
435
+ }
436
+ finally {
437
+ await rm(cwd, { recursive: true, force: true });
438
+ }
439
+ });
440
+ it("does not block oversized Stop stdin for unrelated root autopilot state", async () => {
441
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-stale-root-"));
442
+ try {
443
+ await writeJson(join(cwd, ".omx", "state", "session.json"), {
444
+ session_id: "sess-current-without-active-autopilot",
445
+ cwd,
446
+ });
447
+ await writeJson(join(cwd, ".omx", "state", "autopilot-state.json"), {
448
+ active: true,
449
+ current_phase: "execution",
450
+ });
451
+ const oversizedStop = JSON.stringify({
452
+ hook_event_name: "Stop",
453
+ cwd,
454
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
455
+ });
456
+ assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
457
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
458
+ }
459
+ finally {
460
+ await rm(cwd, { recursive: true, force: true });
461
+ }
462
+ });
463
+ it("does not block oversized Stop stdin when terminal run-state shadows stale autopilot state", async () => {
464
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-terminal-run-"));
465
+ try {
466
+ const sessionId = "sess-cli-stop-oversized-terminal-run";
467
+ await writeActiveAutopilotSession(cwd, sessionId);
468
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "run-state.json"), {
469
+ version: 1,
470
+ active: false,
471
+ mode: "autopilot",
472
+ outcome: "finish",
473
+ lifecycle_outcome: "finished",
474
+ current_phase: "complete",
475
+ completed_at: "2026-05-20T11:00:00.000Z",
476
+ updated_at: "2026-05-20T11:00:00.000Z",
477
+ });
478
+ const oversizedStop = JSON.stringify({
479
+ hook_event_name: "Stop",
480
+ cwd,
481
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
482
+ });
483
+ assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
484
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
485
+ }
486
+ finally {
487
+ await rm(cwd, { recursive: true, force: true });
488
+ }
489
+ });
490
+ it("fails closed for oversized non-Stop stdin before parsing", async () => {
491
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-nonstop-oversized-"));
492
+ try {
493
+ const oversizedPrompt = JSON.stringify({
494
+ hook_event_name: "UserPromptSubmit",
495
+ cwd,
496
+ session_id: "sess-cli-prompt-oversized",
497
+ prompt: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
498
+ });
499
+ const output = parseSingleJsonStdout(runNativeHookCli(oversizedPrompt, { cwd }));
500
+ assert.equal(output.continue, false);
501
+ assert.equal(output.stopReason, "native_hook_stdin_oversized");
502
+ assert.match(String(output.systemMessage ?? ""), /rejected oversized stdin JSON before parsing/);
304
503
  }
305
504
  finally {
306
505
  await rm(cwd, { recursive: true, force: true });
@@ -915,6 +1114,50 @@ describe("codex native hook dispatch", () => {
915
1114
  await rm(cwd, { recursive: true, force: true });
916
1115
  }
917
1116
  });
1117
+ it("keeps a self-parented native role thread as subagent evidence", async () => {
1118
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
1119
+ try {
1120
+ const stateDir = join(cwd, ".omx", "state");
1121
+ const canonicalSessionId = "omx-autopilot-session";
1122
+ const nativeRoleThreadId = "codex-architect-thread";
1123
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1124
+ await writeSessionStart(cwd, canonicalSessionId, {
1125
+ nativeSessionId: nativeRoleThreadId,
1126
+ });
1127
+ const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
1128
+ await writeFile(transcriptPath, `${JSON.stringify({
1129
+ type: "session_meta",
1130
+ payload: {
1131
+ id: nativeRoleThreadId,
1132
+ source: {
1133
+ subagent: {
1134
+ thread_spawn: {
1135
+ parent_thread_id: nativeRoleThreadId,
1136
+ depth: 1,
1137
+ agent_nickname: "Architect",
1138
+ agent_role: "architect",
1139
+ },
1140
+ },
1141
+ },
1142
+ agent_nickname: "Architect",
1143
+ agent_role: "architect",
1144
+ },
1145
+ })}\n`);
1146
+ await dispatchCodexNativeHook({
1147
+ hook_event_name: "SessionStart",
1148
+ cwd,
1149
+ session_id: nativeRoleThreadId,
1150
+ transcript_path: transcriptPath,
1151
+ }, { cwd, sessionOwnerPid: process.pid });
1152
+ const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
1153
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
1154
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
1155
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
1156
+ }
1157
+ finally {
1158
+ await rm(cwd, { recursive: true, force: true });
1159
+ }
1160
+ });
918
1161
  it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
919
1162
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
920
1163
  try {
@@ -1018,6 +1261,97 @@ describe("codex native hook dispatch", () => {
1018
1261
  await rm(cwd, { recursive: true, force: true });
1019
1262
  }
1020
1263
  });
1264
+ it("prefers the OMX owner session id when a native new session revives HUD", async () => {
1265
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-owner-session-revive-"));
1266
+ try {
1267
+ const stateDir = join(cwd, ".omx", "state");
1268
+ const ownerSessionId = "omx-launch-owner-hud";
1269
+ const oldNativeSessionId = "codex-native-hud-old";
1270
+ const nativeSessionId = "codex-native-hud-new";
1271
+ await mkdir(stateDir, { recursive: true });
1272
+ await writeSessionStart(cwd, ownerSessionId, {
1273
+ nativeSessionId: oldNativeSessionId,
1274
+ pid: process.pid,
1275
+ });
1276
+ await dispatchCodexNativeHook({
1277
+ hook_event_name: "SessionStart",
1278
+ cwd,
1279
+ session_id: nativeSessionId,
1280
+ }, {
1281
+ cwd,
1282
+ sessionOwnerPid: process.pid,
1283
+ });
1284
+ const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
1285
+ assert.equal(sessionState.session_id, nativeSessionId);
1286
+ assert.equal(sessionState.native_session_id, nativeSessionId);
1287
+ assert.equal(sessionState.previous_native_session_id, oldNativeSessionId);
1288
+ assert.equal(sessionState.owner_omx_session_id, ownerSessionId);
1289
+ let reconcileCall = null;
1290
+ const promptResult = await dispatchCodexNativeHook({
1291
+ hook_event_name: "UserPromptSubmit",
1292
+ cwd,
1293
+ session_id: nativeSessionId,
1294
+ thread_id: "thread-hud-owner",
1295
+ turn_id: "turn-hud-owner",
1296
+ prompt: "$ralplan fix native new hud owner handoff",
1297
+ }, {
1298
+ cwd,
1299
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
1300
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId, sessionIds: deps.sessionIds };
1301
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
1302
+ },
1303
+ });
1304
+ assert.equal(promptResult.omxEventName, "keyword-detector");
1305
+ assert.deepEqual(reconcileCall, {
1306
+ cwd,
1307
+ sessionId: ownerSessionId,
1308
+ sessionIds: [ownerSessionId, nativeSessionId],
1309
+ });
1310
+ }
1311
+ finally {
1312
+ await rm(cwd, { recursive: true, force: true });
1313
+ }
1314
+ });
1315
+ it("falls back to the canonical session id for malformed HUD owner ids", async () => {
1316
+ for (const [index, invalidOwnerSessionId] of ["codex-native-hud-owner", "omx-../../stale"].entries()) {
1317
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-invalid-owner-revive-"));
1318
+ try {
1319
+ const stateDir = join(cwd, ".omx", "state");
1320
+ const canonicalSessionId = "omx-launch-hud-safe";
1321
+ const nativeSessionId = "codex-native-hud-safe";
1322
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1323
+ await writeSessionStart(cwd, canonicalSessionId);
1324
+ const sessionStatePath = join(stateDir, "session.json");
1325
+ const sessionState = JSON.parse(await readFile(sessionStatePath, "utf-8"));
1326
+ sessionState.owner_omx_session_id = invalidOwnerSessionId;
1327
+ await writeJson(sessionStatePath, sessionState);
1328
+ let reconcileCall = null;
1329
+ const promptResult = await dispatchCodexNativeHook({
1330
+ hook_event_name: "UserPromptSubmit",
1331
+ cwd,
1332
+ session_id: nativeSessionId,
1333
+ thread_id: `thread-hud-invalid-owner-${index}`,
1334
+ turn_id: "turn-hud-invalid-owner",
1335
+ prompt: "$ralplan fix malformed hud owner handoff",
1336
+ }, {
1337
+ cwd,
1338
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
1339
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId, sessionIds: deps.sessionIds };
1340
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
1341
+ },
1342
+ });
1343
+ assert.equal(promptResult.omxEventName, "keyword-detector");
1344
+ assert.deepEqual(reconcileCall, {
1345
+ cwd,
1346
+ sessionId: canonicalSessionId,
1347
+ sessionIds: [canonicalSessionId, nativeSessionId],
1348
+ });
1349
+ }
1350
+ finally {
1351
+ await rm(cwd, { recursive: true, force: true });
1352
+ }
1353
+ }
1354
+ });
1021
1355
  it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
1022
1356
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
1023
1357
  try {
@@ -2208,75 +2542,376 @@ standardMaxRounds = 15
2208
2542
  await rm(cwd, { recursive: true, force: true });
2209
2543
  }
2210
2544
  });
2211
- it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
2212
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
2213
- try {
2214
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2215
- const result = await dispatchCodexNativeHook({
2216
- hook_event_name: "UserPromptSubmit",
2217
- cwd,
2218
- session_id: "sess-plugin-1",
2219
- thread_id: "thread-plugin-1",
2220
- turn_id: "turn-plugin-1",
2221
- prompt: "$oh-my-codex:ralplan implement issue #1307",
2222
- }, { cwd });
2223
- assert.equal(result.omxEventName, "keyword-detector");
2224
- assert.equal(result.skillState?.skill, "ralplan");
2225
- const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2226
- assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
2227
- assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
2228
- assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
2229
- }
2230
- finally {
2231
- await rm(cwd, { recursive: true, force: true });
2232
- }
2233
- });
2234
- it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
2235
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
2545
+ it("does not treat a corrupt leader kind=subagent tracker entry as native subagent prompt scope", async () => {
2546
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-corrupt-leader-subagent-"));
2236
2547
  try {
2237
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2548
+ const stateDir = join(cwd, ".omx", "state");
2549
+ const canonicalSessionId = "sess-corrupt-leader";
2550
+ const leaderNativeSessionId = "native-corrupt-leader";
2551
+ const nowIso = new Date().toISOString();
2552
+ await writeJson(join(stateDir, "session.json"), {
2553
+ session_id: canonicalSessionId,
2554
+ native_session_id: leaderNativeSessionId,
2555
+ });
2556
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2557
+ schemaVersion: 1,
2558
+ sessions: {
2559
+ [canonicalSessionId]: {
2560
+ session_id: canonicalSessionId,
2561
+ leader_thread_id: leaderNativeSessionId,
2562
+ updated_at: nowIso,
2563
+ threads: {
2564
+ [leaderNativeSessionId]: {
2565
+ thread_id: leaderNativeSessionId,
2566
+ kind: "subagent",
2567
+ first_seen_at: nowIso,
2568
+ last_seen_at: nowIso,
2569
+ turn_count: 2,
2570
+ },
2571
+ },
2572
+ },
2573
+ },
2574
+ });
2238
2575
  const result = await dispatchCodexNativeHook({
2239
2576
  hook_event_name: "UserPromptSubmit",
2240
2577
  cwd,
2241
- session_id: "sess-autopilot-ralplan-gate",
2242
- thread_id: "thread-autopilot-ralplan-gate",
2243
- turn_id: "turn-autopilot-ralplan-gate",
2244
- prompt: "$autopilot implement issue #2430",
2578
+ session_id: leaderNativeSessionId,
2579
+ thread_id: leaderNativeSessionId,
2580
+ turn_id: "turn-corrupt-leader",
2581
+ prompt: "$autopilot continue this review blocker fix",
2245
2582
  }, { cwd });
2246
2583
  assert.equal(result.omxEventName, "keyword-detector");
2247
2584
  assert.equal(result.skillState?.skill, "autopilot");
2248
- const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2249
- assert.match(message, /Autopilot protocol:/);
2250
- assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
2251
- assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
2252
- assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
2585
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2253
2586
  }
2254
2587
  finally {
2255
2588
  await rm(cwd, { recursive: true, force: true });
2256
2589
  }
2257
2590
  });
2258
- it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
2259
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
2591
+ it("lets the current canonical leader boundary beat stale global subagent tracking with a distinct prompt thread id", async () => {
2592
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-leader-stale-global-"));
2260
2593
  try {
2261
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2262
- const result = await dispatchCodexNativeHook({
2263
- hook_event_name: "UserPromptSubmit",
2264
- cwd,
2265
- session_id: "sess-ultragoal-1",
2266
- thread_id: "thread-ultragoal-1",
2267
- turn_id: "turn-ultragoal-1",
2268
- prompt: "$ultragoal split this launch into durable goals",
2269
- }, { cwd });
2270
- assert.equal(result.omxEventName, "keyword-detector");
2271
- assert.equal(result.skillState?.skill, "ultragoal");
2272
- assert.equal(result.skillState?.initialized_mode, "ultragoal");
2273
- assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-ultragoal-1/ultragoal-state.json");
2274
- const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2275
- assert.match(message, /"\$ultragoal" -> ultragoal/);
2276
- assert.match(message, /Ultragoal protocol:/);
2277
- assert.match(message, /get_goal/);
2278
- assert.match(message, /create_goal/);
2279
- assert.match(message, /update_goal/);
2594
+ const stateDir = join(cwd, ".omx", "state");
2595
+ const canonicalSessionId = "sess-current-leader";
2596
+ const leaderNativeSessionId = "native-current-leader";
2597
+ const staleSessionId = "sess-stale-subagent";
2598
+ const staleLeaderNativeSessionId = "native-stale-leader";
2599
+ const nowIso = new Date().toISOString();
2600
+ await writeJson(join(stateDir, "session.json"), {
2601
+ session_id: canonicalSessionId,
2602
+ native_session_id: leaderNativeSessionId,
2603
+ });
2604
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2605
+ schemaVersion: 1,
2606
+ sessions: {
2607
+ [canonicalSessionId]: {
2608
+ session_id: canonicalSessionId,
2609
+ leader_thread_id: leaderNativeSessionId,
2610
+ updated_at: nowIso,
2611
+ threads: {
2612
+ [leaderNativeSessionId]: {
2613
+ thread_id: leaderNativeSessionId,
2614
+ kind: "leader",
2615
+ first_seen_at: nowIso,
2616
+ last_seen_at: nowIso,
2617
+ turn_count: 1,
2618
+ },
2619
+ },
2620
+ },
2621
+ [staleSessionId]: {
2622
+ session_id: staleSessionId,
2623
+ leader_thread_id: staleLeaderNativeSessionId,
2624
+ updated_at: nowIso,
2625
+ threads: {
2626
+ [staleLeaderNativeSessionId]: {
2627
+ thread_id: staleLeaderNativeSessionId,
2628
+ kind: "leader",
2629
+ first_seen_at: nowIso,
2630
+ last_seen_at: nowIso,
2631
+ turn_count: 1,
2632
+ },
2633
+ [leaderNativeSessionId]: {
2634
+ thread_id: leaderNativeSessionId,
2635
+ kind: "subagent",
2636
+ first_seen_at: nowIso,
2637
+ last_seen_at: nowIso,
2638
+ turn_count: 1,
2639
+ mode: "architect",
2640
+ },
2641
+ },
2642
+ },
2643
+ },
2644
+ });
2645
+ const result = await dispatchCodexNativeHook({
2646
+ hook_event_name: "UserPromptSubmit",
2647
+ cwd,
2648
+ session_id: leaderNativeSessionId,
2649
+ thread_id: "thread-current-turn-not-native-session",
2650
+ turn_id: "turn-current-leader",
2651
+ prompt: "$autopilot continue",
2652
+ }, { cwd });
2653
+ assert.equal(result.omxEventName, "keyword-detector");
2654
+ assert.equal(result.skillState?.skill, "autopilot");
2655
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2656
+ assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
2657
+ }
2658
+ finally {
2659
+ await rm(cwd, { recursive: true, force: true });
2660
+ }
2661
+ });
2662
+ it("lets the current session native leader beat stale global subagent tracking without a canonical summary and with a distinct prompt thread id", async () => {
2663
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-stale-global-"));
2664
+ try {
2665
+ const stateDir = join(cwd, ".omx", "state");
2666
+ const canonicalSessionId = "sess-current-native-leader";
2667
+ const leaderNativeSessionId = "native-current-leader-no-summary";
2668
+ const staleSessionId = "sess-stale-native-subagent";
2669
+ const staleLeaderNativeSessionId = "native-stale-parent";
2670
+ const nowIso = new Date().toISOString();
2671
+ await writeJson(join(stateDir, "session.json"), {
2672
+ session_id: canonicalSessionId,
2673
+ native_session_id: leaderNativeSessionId,
2674
+ });
2675
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2676
+ schemaVersion: 1,
2677
+ sessions: {
2678
+ [staleSessionId]: {
2679
+ session_id: staleSessionId,
2680
+ leader_thread_id: staleLeaderNativeSessionId,
2681
+ updated_at: nowIso,
2682
+ threads: {
2683
+ [staleLeaderNativeSessionId]: {
2684
+ thread_id: staleLeaderNativeSessionId,
2685
+ kind: "leader",
2686
+ first_seen_at: nowIso,
2687
+ last_seen_at: nowIso,
2688
+ turn_count: 1,
2689
+ },
2690
+ [leaderNativeSessionId]: {
2691
+ thread_id: leaderNativeSessionId,
2692
+ kind: "subagent",
2693
+ first_seen_at: nowIso,
2694
+ last_seen_at: nowIso,
2695
+ turn_count: 1,
2696
+ mode: "critic",
2697
+ },
2698
+ },
2699
+ },
2700
+ },
2701
+ });
2702
+ const result = await dispatchCodexNativeHook({
2703
+ hook_event_name: "UserPromptSubmit",
2704
+ cwd,
2705
+ session_id: leaderNativeSessionId,
2706
+ thread_id: "thread-current-turn-not-native-session",
2707
+ turn_id: "turn-current-native-leader",
2708
+ prompt: "$autopilot continue",
2709
+ }, { cwd });
2710
+ assert.equal(result.omxEventName, "keyword-detector");
2711
+ assert.equal(result.skillState?.skill, "autopilot");
2712
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2713
+ assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
2714
+ }
2715
+ finally {
2716
+ await rm(cwd, { recursive: true, force: true });
2717
+ }
2718
+ });
2719
+ it("lets the current session native leader beat a malformed canonical subagent entry", async () => {
2720
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-malformed-canonical-"));
2721
+ try {
2722
+ const stateDir = join(cwd, ".omx", "state");
2723
+ const canonicalSessionId = "sess-current-native-leader-malformed";
2724
+ const leaderNativeSessionId = "native-current-leader-malformed";
2725
+ const nowIso = new Date().toISOString();
2726
+ await writeJson(join(stateDir, "session.json"), {
2727
+ session_id: canonicalSessionId,
2728
+ native_session_id: leaderNativeSessionId,
2729
+ });
2730
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2731
+ schemaVersion: 1,
2732
+ sessions: {
2733
+ [canonicalSessionId]: {
2734
+ session_id: canonicalSessionId,
2735
+ updated_at: nowIso,
2736
+ threads: {
2737
+ [leaderNativeSessionId]: {
2738
+ thread_id: leaderNativeSessionId,
2739
+ kind: "subagent",
2740
+ first_seen_at: nowIso,
2741
+ last_seen_at: nowIso,
2742
+ turn_count: 1,
2743
+ mode: "architect",
2744
+ },
2745
+ },
2746
+ },
2747
+ },
2748
+ });
2749
+ const result = await dispatchCodexNativeHook({
2750
+ hook_event_name: "UserPromptSubmit",
2751
+ cwd,
2752
+ session_id: leaderNativeSessionId,
2753
+ thread_id: leaderNativeSessionId,
2754
+ turn_id: "turn-current-native-leader-malformed",
2755
+ prompt: "$autopilot continue",
2756
+ }, { cwd });
2757
+ assert.equal(result.omxEventName, "keyword-detector");
2758
+ assert.equal(result.skillState?.skill, "autopilot");
2759
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2760
+ }
2761
+ finally {
2762
+ await rm(cwd, { recursive: true, force: true });
2763
+ }
2764
+ });
2765
+ it("still treats mixed child and leader payload identities as native subagent scope", async () => {
2766
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-mixed-child-leader-identity-"));
2767
+ try {
2768
+ const stateDir = join(cwd, ".omx", "state");
2769
+ const canonicalSessionId = "sess-mixed-child-leader";
2770
+ const leaderNativeSessionId = "native-mixed-leader";
2771
+ const childNativeSessionId = "native-mixed-child";
2772
+ const nowIso = new Date().toISOString();
2773
+ await writeJson(join(stateDir, "session.json"), {
2774
+ session_id: canonicalSessionId,
2775
+ native_session_id: leaderNativeSessionId,
2776
+ });
2777
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2778
+ schemaVersion: 1,
2779
+ sessions: {
2780
+ [canonicalSessionId]: {
2781
+ session_id: canonicalSessionId,
2782
+ leader_thread_id: leaderNativeSessionId,
2783
+ updated_at: nowIso,
2784
+ threads: {
2785
+ [leaderNativeSessionId]: {
2786
+ thread_id: leaderNativeSessionId,
2787
+ kind: "leader",
2788
+ first_seen_at: nowIso,
2789
+ last_seen_at: nowIso,
2790
+ turn_count: 1,
2791
+ },
2792
+ [childNativeSessionId]: {
2793
+ thread_id: childNativeSessionId,
2794
+ kind: "subagent",
2795
+ first_seen_at: nowIso,
2796
+ last_seen_at: nowIso,
2797
+ turn_count: 1,
2798
+ mode: "critic",
2799
+ },
2800
+ },
2801
+ },
2802
+ },
2803
+ });
2804
+ const result = await dispatchCodexNativeHook({
2805
+ hook_event_name: "UserPromptSubmit",
2806
+ cwd,
2807
+ session_id: childNativeSessionId,
2808
+ thread_id: leaderNativeSessionId,
2809
+ turn_id: "turn-mixed-child-leader",
2810
+ prompt: "$ralplan review this as delegated text",
2811
+ }, { cwd });
2812
+ assert.equal(result.omxEventName, "keyword-detector");
2813
+ assert.equal(result.skillState, null);
2814
+ assert.equal(result.outputJson, null);
2815
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
2816
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
2817
+ const reversedResult = await dispatchCodexNativeHook({
2818
+ hook_event_name: "UserPromptSubmit",
2819
+ cwd,
2820
+ session_id: leaderNativeSessionId,
2821
+ thread_id: childNativeSessionId,
2822
+ turn_id: "turn-mixed-leader-child",
2823
+ prompt: "$autopilot review this as delegated text",
2824
+ }, { cwd });
2825
+ assert.equal(reversedResult.omxEventName, "keyword-detector");
2826
+ assert.equal(reversedResult.skillState, null);
2827
+ assert.equal(reversedResult.outputJson, null);
2828
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), false);
2829
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "autopilot-state.json")), false);
2830
+ }
2831
+ finally {
2832
+ await rm(cwd, { recursive: true, force: true });
2833
+ }
2834
+ });
2835
+ it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
2836
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
2837
+ try {
2838
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2839
+ const result = await dispatchCodexNativeHook({
2840
+ hook_event_name: "UserPromptSubmit",
2841
+ cwd,
2842
+ session_id: "sess-plugin-1",
2843
+ thread_id: "thread-plugin-1",
2844
+ turn_id: "turn-plugin-1",
2845
+ prompt: "$oh-my-codex:ralplan implement issue #1307",
2846
+ }, { cwd });
2847
+ assert.equal(result.omxEventName, "keyword-detector");
2848
+ assert.equal(result.skillState?.skill, "ralplan");
2849
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2850
+ assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
2851
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
2852
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
2853
+ }
2854
+ finally {
2855
+ await rm(cwd, { recursive: true, force: true });
2856
+ }
2857
+ });
2858
+ it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
2859
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
2860
+ try {
2861
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2862
+ const result = await dispatchCodexNativeHook({
2863
+ hook_event_name: "UserPromptSubmit",
2864
+ cwd,
2865
+ session_id: "sess-autopilot-ralplan-gate",
2866
+ thread_id: "thread-autopilot-ralplan-gate",
2867
+ turn_id: "turn-autopilot-ralplan-gate",
2868
+ prompt: "$autopilot implement issue #2430",
2869
+ }, { cwd });
2870
+ assert.equal(result.omxEventName, "keyword-detector");
2871
+ assert.equal(result.skillState?.skill, "autopilot");
2872
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2873
+ assert.match(message, /Autopilot protocol:/);
2874
+ assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
2875
+ assert.match(message, /structured question chain, not a one-question gate/);
2876
+ assert.match(message, /re-score ambiguity against the active threshold/);
2877
+ assert.match(message, /max_rounds as a cap/);
2878
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
2879
+ assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
2880
+ assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
2881
+ const autopilotState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", "sess-autopilot-ralplan-gate", "autopilot-state.json"), "utf-8"));
2882
+ const snapshotPath = autopilotState.state?.handoff_artifacts?.context_snapshot_path ?? "";
2883
+ assert.match(snapshotPath, /^\.omx\/context\/implement-issue-2430-\d{8}T\d{6}Z\.md$/);
2884
+ const snapshot = await readFile(join(cwd, snapshotPath), "utf-8");
2885
+ assert.match(snapshot, /activation prompt \/ task seed: \$autopilot implement issue #2430/);
2886
+ assert.match(snapshot, /scope note: this seed captures the Autopilot activation prompt/);
2887
+ assert.match(snapshot, /constraints: follow deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
2888
+ }
2889
+ finally {
2890
+ await rm(cwd, { recursive: true, force: true });
2891
+ }
2892
+ });
2893
+ it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
2894
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
2895
+ try {
2896
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2897
+ const result = await dispatchCodexNativeHook({
2898
+ hook_event_name: "UserPromptSubmit",
2899
+ cwd,
2900
+ session_id: "sess-ultragoal-1",
2901
+ thread_id: "thread-ultragoal-1",
2902
+ turn_id: "turn-ultragoal-1",
2903
+ prompt: "$ultragoal split this launch into durable goals",
2904
+ }, { cwd });
2905
+ assert.equal(result.omxEventName, "keyword-detector");
2906
+ assert.equal(result.skillState?.skill, "ultragoal");
2907
+ assert.equal(result.skillState?.initialized_mode, "ultragoal");
2908
+ assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-ultragoal-1/ultragoal-state.json");
2909
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2910
+ assert.match(message, /"\$ultragoal" -> ultragoal/);
2911
+ assert.match(message, /Ultragoal protocol:/);
2912
+ assert.match(message, /get_goal/);
2913
+ assert.match(message, /create_goal/);
2914
+ assert.match(message, /update_goal/);
2280
2915
  assert.match(message, /does not call `\/goal clear`/);
2281
2916
  assert.match(message, /multiple sequential ultragoal runs/);
2282
2917
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), true);
@@ -2705,6 +3340,11 @@ ${JSON.stringify({
2705
3340
  assert.equal(result.skillState?.skill, "autopilot");
2706
3341
  const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2707
3342
  assert.match(message, /"keep going" -> ralph/);
3343
+ assert.match(message, /Autopilot protocol:/);
3344
+ assert.match(message, /structured question chain, not a one-question gate/);
3345
+ assert.match(message, /re-score ambiguity against the active threshold/);
3346
+ assert.match(message, /max_rounds as a cap/);
3347
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
2708
3348
  assert.doesNotMatch(message, /denied workflow keyword/i);
2709
3349
  assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
2710
3350
  assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
@@ -2714,6 +3354,107 @@ ${JSON.stringify({
2714
3354
  await rm(cwd, { recursive: true, force: true });
2715
3355
  }
2716
3356
  });
3357
+ it("keeps omx question answers on the active autopilot skill so the interview chain guidance is injected", async () => {
3358
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-question-answer-continuation-"));
3359
+ try {
3360
+ const sessionId = "sess-autopilot-question-answer";
3361
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
3362
+ await mkdir(sessionDir, { recursive: true });
3363
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
3364
+ version: 1,
3365
+ active: true,
3366
+ skill: "autopilot",
3367
+ keyword: "$autopilot",
3368
+ phase: "deep-interview",
3369
+ initialized_mode: "autopilot",
3370
+ initialized_state_path: `.omx/state/sessions/${sessionId}/autopilot-state.json`,
3371
+ session_id: sessionId,
3372
+ active_skills: [
3373
+ { skill: "autopilot", phase: "deep-interview", active: true, session_id: sessionId },
3374
+ ],
3375
+ });
3376
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
3377
+ active: true,
3378
+ mode: "autopilot",
3379
+ current_phase: "deep-interview",
3380
+ started_at: "2026-04-19T00:00:00.000Z",
3381
+ updated_at: "2026-04-19T00:10:00.000Z",
3382
+ session_id: sessionId,
3383
+ });
3384
+ const result = await dispatchCodexNativeHook({
3385
+ hook_event_name: "UserPromptSubmit",
3386
+ cwd,
3387
+ session_id: sessionId,
3388
+ thread_id: "thread-autopilot-question-answer",
3389
+ turn_id: "turn-autopilot-question-answer",
3390
+ prompt: "[omx question answered] semantic_marker_expansion $ralplan",
3391
+ }, { cwd });
3392
+ assert.equal(result.omxEventName, "keyword-detector");
3393
+ assert.equal(result.skillState?.skill, "autopilot");
3394
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
3395
+ assert.match(message, /continued active workflow skill "autopilot"/);
3396
+ assert.match(message, /Autopilot protocol:/);
3397
+ assert.match(message, /structured question chain, not a one-question gate/);
3398
+ assert.match(message, /This turn is a marked omx question answer/);
3399
+ assert.match(message, /then re-score/);
3400
+ assert.match(message, /write interview_complete evidence and hand off/);
3401
+ assert.match(message, /readiness gate remains unresolved and the answer would materially change execution/);
3402
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
3403
+ assert.doesNotMatch(message, /denied workflow keyword/i);
3404
+ assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
3405
+ }
3406
+ finally {
3407
+ await rm(cwd, { recursive: true, force: true });
3408
+ }
3409
+ });
3410
+ it("keeps deep-interview bridge guidance on marked question answers with workflow-like tokens", async () => {
3411
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-question-answer-continuation-"));
3412
+ try {
3413
+ const sessionId = "sess-deep-interview-question-answer";
3414
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
3415
+ await mkdir(sessionDir, { recursive: true });
3416
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
3417
+ version: 1,
3418
+ active: true,
3419
+ skill: "deep-interview",
3420
+ keyword: "$deep-interview",
3421
+ phase: "planning",
3422
+ initialized_mode: "deep-interview",
3423
+ initialized_state_path: `.omx/state/sessions/${sessionId}/deep-interview-state.json`,
3424
+ session_id: sessionId,
3425
+ active_skills: [
3426
+ { skill: "deep-interview", phase: "planning", active: true, session_id: sessionId },
3427
+ ],
3428
+ });
3429
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
3430
+ active: true,
3431
+ mode: "deep-interview",
3432
+ current_phase: "intent-first",
3433
+ started_at: "2026-04-21T10:00:00.000Z",
3434
+ updated_at: "2026-04-21T10:00:00.000Z",
3435
+ });
3436
+ const result = await dispatchCodexNativeHook({
3437
+ hook_event_name: "UserPromptSubmit",
3438
+ cwd,
3439
+ session_id: sessionId,
3440
+ thread_id: "thread-deep-interview-question-answer",
3441
+ turn_id: "turn-deep-interview-question-answer",
3442
+ prompt: "[omx question answered] answer text $ralplan",
3443
+ }, { cwd });
3444
+ assert.equal(result.omxEventName, "keyword-detector");
3445
+ assert.equal(result.skillState?.skill, "deep-interview");
3446
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
3447
+ assert.match(message, /continued active workflow skill "deep-interview"/);
3448
+ assert.match(message, /workflow-like tokens inside the marked omx question answer are treated as answer text/);
3449
+ assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
3450
+ assert.match(message, /native structured question tool when available/);
3451
+ assert.doesNotMatch(message, /detected workflow keyword "\$ralplan" -> ralplan/);
3452
+ assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
3453
+ }
3454
+ finally {
3455
+ await rm(cwd, { recursive: true, force: true });
3456
+ }
3457
+ });
2717
3458
  it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
2718
3459
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
2719
3460
  try {
@@ -3140,6 +3881,10 @@ export async function onHookEvent(event) {
3140
3881
  active: true,
3141
3882
  mode: "deep-interview",
3142
3883
  current_phase: "intent-first",
3884
+ deep_interview_gate: {
3885
+ status: "complete",
3886
+ rationale: "Requirements are clarified and ready for ralplan consensus.",
3887
+ },
3143
3888
  });
3144
3889
  await writeJson(join(sessionDir, "skill-active-state.json"), {
3145
3890
  active: true,
@@ -3459,7 +4204,54 @@ esac
3459
4204
  await rm(cwd, { recursive: true, force: true });
3460
4205
  }
3461
4206
  });
3462
- it("reuses an existing owner-tagged HUD pane when UserPromptSubmit revives with the canonical session id", async () => {
4207
+ it("skips prompt-submit HUD reconciliation during doctor smoke validation", async () => {
4208
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-doctor-smoke-hud-"));
4209
+ const originalTmux = process.env.TMUX;
4210
+ const originalTmuxPane = process.env.TMUX_PANE;
4211
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
4212
+ const originalDoctorSmoke = process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE;
4213
+ try {
4214
+ process.env.TMUX = "1";
4215
+ process.env.TMUX_PANE = "%1";
4216
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
4217
+ process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE = "1";
4218
+ let reconcileCalled = false;
4219
+ const result = await dispatchCodexNativeHook({
4220
+ hook_event_name: "UserPromptSubmit",
4221
+ cwd,
4222
+ session_id: "omx-doctor-plugin-hook-smoke",
4223
+ prompt: "$ralplan doctor plugin hook smoke test",
4224
+ }, {
4225
+ cwd,
4226
+ reconcileHudForPromptSubmitFn: async () => {
4227
+ reconcileCalled = true;
4228
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
4229
+ },
4230
+ });
4231
+ assert.equal(result.omxEventName, "keyword-detector");
4232
+ assert.equal(reconcileCalled, false);
4233
+ }
4234
+ finally {
4235
+ if (originalTmux === undefined)
4236
+ delete process.env.TMUX;
4237
+ else
4238
+ process.env.TMUX = originalTmux;
4239
+ if (originalTmuxPane === undefined)
4240
+ delete process.env.TMUX_PANE;
4241
+ else
4242
+ process.env.TMUX_PANE = originalTmuxPane;
4243
+ if (originalHudOwner === undefined)
4244
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
4245
+ else
4246
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
4247
+ if (originalDoctorSmoke === undefined)
4248
+ delete process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE;
4249
+ else
4250
+ process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE = originalDoctorSmoke;
4251
+ await rm(cwd, { recursive: true, force: true });
4252
+ }
4253
+ });
4254
+ it("recreates a leader-only HUD pane when UserPromptSubmit revives with the canonical session id", async () => {
3463
4255
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-"));
3464
4256
  const originalTmux = process.env.TMUX;
3465
4257
  const originalTmuxPane = process.env.TMUX_PANE;
@@ -3506,8 +4298,8 @@ esac
3506
4298
  assert.equal(result.omxEventName, "keyword-detector");
3507
4299
  const tmuxCalls = await readFile(tmuxLog, "utf-8");
3508
4300
  assert.match(tmuxCalls, /list-panes -t %1 -F/);
3509
- assert.match(tmuxCalls, new RegExp(`resize-pane -t %2 -y ${HUD_TMUX_HEIGHT_LINES}`));
3510
- assert.doesNotMatch(tmuxCalls, /split-window/);
4301
+ assert.match(tmuxCalls, /split-window/);
4302
+ assert.match(tmuxCalls, new RegExp(`resize-pane -t %9 -y ${HUD_TMUX_HEIGHT_LINES}`));
3511
4303
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", canonicalSessionId, "ralplan-state.json")), true);
3512
4304
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", nativeSessionId, "ralplan-state.json")), false);
3513
4305
  }
@@ -5758,6 +6550,42 @@ exit 0
5758
6550
  await rm(cwd, { recursive: true, force: true });
5759
6551
  }
5760
6552
  });
6553
+ it("does not block ordinary non-zero grep output in PostToolUse", async () => {
6554
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-grep-nonzero-"));
6555
+ try {
6556
+ const result = await dispatchCodexNativeHook({
6557
+ hook_event_name: "PostToolUse",
6558
+ cwd,
6559
+ tool_name: "Bash",
6560
+ tool_use_id: "tool-grep-nonzero",
6561
+ tool_input: { command: "grep -R missing-pattern src | head -20" },
6562
+ tool_response: "{\"exit_code\":1,\"stdout\":\"src/example.ts:TODO\",\"stderr\":\"\"}",
6563
+ }, { cwd });
6564
+ assert.equal(result.omxEventName, "post-tool-use");
6565
+ assert.equal(result.outputJson, null);
6566
+ }
6567
+ finally {
6568
+ await rm(cwd, { recursive: true, force: true });
6569
+ }
6570
+ });
6571
+ it("does not block ordinary non-zero diagnostic output in PostToolUse", async () => {
6572
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-diagnostic-nonzero-"));
6573
+ try {
6574
+ const result = await dispatchCodexNativeHook({
6575
+ hook_event_name: "PostToolUse",
6576
+ cwd,
6577
+ tool_name: "Bash",
6578
+ tool_use_id: "tool-diagnostic-nonzero",
6579
+ tool_input: { command: "find src -name nope -print" },
6580
+ tool_response: "{\"exit_code\":1,\"stdout\":\"searched 10 files\",\"stderr\":\"\"}",
6581
+ }, { cwd });
6582
+ assert.equal(result.omxEventName, "post-tool-use");
6583
+ assert.equal(result.outputJson, null);
6584
+ }
6585
+ finally {
6586
+ await rm(cwd, { recursive: true, force: true });
6587
+ }
6588
+ });
5761
6589
  it("treats stderr-only informative non-zero output as reviewable instead of a generic failure", async () => {
5762
6590
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-informative-stderr-"));
5763
6591
  try {
@@ -5808,6 +6636,72 @@ exit 0
5808
6636
  await rm(cwd, { recursive: true, force: true });
5809
6637
  }
5810
6638
  });
6639
+ it("treats wrapped gh pr checks output as reviewable", async () => {
6640
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-wrapped-"));
6641
+ try {
6642
+ for (const command of [
6643
+ "GH_PAGER=cat gh pr checks",
6644
+ "env GH_TOKEN=ghp_testtoken gh pr checks",
6645
+ "/usr/bin/env gh pr checks",
6646
+ "env -- gh pr checks",
6647
+ "env -C repo gh pr checks",
6648
+ "/usr/bin/gh pr checks",
6649
+ "gh --repo owner/repo pr checks",
6650
+ "echo a; gh pr checks",
6651
+ "cd repo && gh pr checks",
6652
+ ]) {
6653
+ const result = await dispatchCodexNativeHook({
6654
+ hook_event_name: "PostToolUse",
6655
+ cwd,
6656
+ tool_name: "Bash",
6657
+ tool_use_id: `tool-useful-${command}`,
6658
+ tool_input: { command },
6659
+ tool_response: "{\"exit_code\":8,\"stdout\":\"build pending\",\"stderr\":\"\"}",
6660
+ }, { cwd });
6661
+ assert.equal(result.omxEventName, "post-tool-use");
6662
+ assert.equal(result.outputJson?.decision, "block", command);
6663
+ }
6664
+ }
6665
+ finally {
6666
+ await rm(cwd, { recursive: true, force: true });
6667
+ }
6668
+ });
6669
+ it("does not treat heredoc gh pr checks text as a reviewable command", async () => {
6670
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-heredoc-"));
6671
+ try {
6672
+ const result = await dispatchCodexNativeHook({
6673
+ hook_event_name: "PostToolUse",
6674
+ cwd,
6675
+ tool_name: "Bash",
6676
+ tool_use_id: "tool-heredoc-gh-checks",
6677
+ tool_input: { command: "cat <<'EOF'\ngh pr checks\nEOF\nfalse" },
6678
+ tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
6679
+ }, { cwd });
6680
+ assert.equal(result.omxEventName, "post-tool-use");
6681
+ assert.equal(result.outputJson, null);
6682
+ }
6683
+ finally {
6684
+ await rm(cwd, { recursive: true, force: true });
6685
+ }
6686
+ });
6687
+ it("does not treat echoed gh pr checks text as a reviewable command", async () => {
6688
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-echo-"));
6689
+ try {
6690
+ const result = await dispatchCodexNativeHook({
6691
+ hook_event_name: "PostToolUse",
6692
+ cwd,
6693
+ tool_name: "Bash",
6694
+ tool_use_id: "tool-echo-gh-checks",
6695
+ tool_input: { command: "echo gh pr checks" },
6696
+ tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
6697
+ }, { cwd });
6698
+ assert.equal(result.omxEventName, "post-tool-use");
6699
+ assert.equal(result.outputJson, null);
6700
+ }
6701
+ finally {
6702
+ await rm(cwd, { recursive: true, force: true });
6703
+ }
6704
+ });
5811
6705
  it("returns MCP transport-death guidance and preserves failed team state", async () => {
5812
6706
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
5813
6707
  try {
@@ -6649,63 +7543,49 @@ exit 0
6649
7543
  await rm(cwd, { recursive: true, force: true });
6650
7544
  }
6651
7545
  });
6652
- it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
6653
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
6654
- const prevTeamWorker = process.env.OMX_TEAM_WORKER;
6655
- const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
7546
+ it("dedupes allowed worker Stop leader nudges across workers in the same team window", async () => {
7547
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-team-dedupe-"));
6656
7548
  const prevPath = process.env.PATH;
6657
7549
  try {
6658
- await initTeamState("worker-stop-helper-fail", "worker stop helper failure", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" });
7550
+ const stateDir = join(cwd, ".omx", "state");
7551
+ const logsDir = join(cwd, ".omx", "logs");
7552
+ const teamName = "worker-stop-team-dedupe";
7553
+ const teamDir = join(stateDir, "team", teamName);
6659
7554
  const fakeBinDir = join(cwd, "fake-bin");
7555
+ const tmuxLogPath = join(cwd, "tmux.log");
6660
7556
  await mkdir(fakeBinDir, { recursive: true });
6661
- await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
7557
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
6662
7558
  await chmod(join(fakeBinDir, "tmux"), 0o755);
6663
- const stateDir = join(cwd, ".omx", "state");
6664
- const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
6665
- await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
6666
- name: "worker-stop-helper-fail",
6667
- tmux_session: "omx-team-worker-stop",
6668
- leader_pane_id: "%42",
6669
- workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
6670
- });
6671
- await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
6672
- name: "worker-stop-helper-fail",
7559
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7560
+ name: teamName,
6673
7561
  tmux_session: "omx-team-worker-stop",
6674
7562
  leader_pane_id: "%42",
6675
- workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
6676
- });
6677
- await writeJson(join(workerDir, "identity.json"), {
6678
- name: "worker-1",
6679
- assigned_tasks: ["1"],
6680
- team_state_root: stateDir,
6681
- });
6682
- await writeJson(join(workerDir, "status.json"), {
6683
- state: "done",
6684
- current_task_id: "1",
6685
- updated_at: new Date().toISOString(),
6686
- });
6687
- await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
6688
- id: "1",
6689
- status: "completed",
6690
- owner: "worker-1",
7563
+ workers: [
7564
+ { name: "worker-1", index: 1, pane_id: "%10" },
7565
+ { name: "worker-2", index: 2, pane_id: "%11" },
7566
+ ],
6691
7567
  });
6692
- process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
6693
- process.env.OMX_TEAM_STATE_ROOT = stateDir;
6694
7568
  process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
6695
- const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" }, { cwd });
6696
- assert.equal(result.outputJson, null);
6697
- const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
6698
- assert.equal(nudgeState.delivery, "deferred");
7569
+ const first = await maybeNudgeLeaderForAllowedWorkerStop({
7570
+ stateDir,
7571
+ logsDir,
7572
+ workerContext: { teamName, workerName: "worker-1" },
7573
+ });
7574
+ const second = await maybeNudgeLeaderForAllowedWorkerStop({
7575
+ stateDir,
7576
+ logsDir,
7577
+ workerContext: { teamName, workerName: "worker-2" },
7578
+ });
7579
+ assert.equal(first.result, "sent");
7580
+ assert.equal(second.result, "suppressed_team_cooldown");
7581
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7582
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
7583
+ assert.equal(stopNudges.length, 1, "same-team workers should share one leader nudge cooldown window");
7584
+ const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
7585
+ assert.equal(teamNudgeState.worker, "worker-1");
7586
+ assert.equal(teamNudgeState.delivery, "sent");
6699
7587
  }
6700
7588
  finally {
6701
- if (typeof prevTeamWorker === "string")
6702
- process.env.OMX_TEAM_WORKER = prevTeamWorker;
6703
- else
6704
- delete process.env.OMX_TEAM_WORKER;
6705
- if (typeof prevTeamStateRoot === "string")
6706
- process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
6707
- else
6708
- delete process.env.OMX_TEAM_STATE_ROOT;
6709
7589
  if (typeof prevPath === "string")
6710
7590
  process.env.PATH = prevPath;
6711
7591
  else
@@ -6713,45 +7593,360 @@ exit 0
6713
7593
  await rm(cwd, { recursive: true, force: true });
6714
7594
  }
6715
7595
  });
6716
- it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
6717
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
6718
- const prevTeamWorker = process.env.OMX_TEAM_WORKER;
6719
- const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
6720
- const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
7596
+ it("serializes concurrent allowed worker Stop leader nudges with a team lock", async () => {
7597
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-concurrent-dedupe-"));
6721
7598
  const prevPath = process.env.PATH;
6722
7599
  try {
6723
- await initTeamState("worker-stop-failed-task", "worker stop failed task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" });
7600
+ const stateDir = join(cwd, ".omx", "state");
7601
+ const logsDir = join(cwd, ".omx", "logs");
7602
+ const teamName = "worker-stop-concurrent";
7603
+ const teamDir = join(stateDir, "team", teamName);
6724
7604
  const fakeBinDir = join(cwd, "fake-bin");
6725
7605
  const tmuxLogPath = join(cwd, "tmux.log");
6726
7606
  await mkdir(fakeBinDir, { recursive: true });
6727
- await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
7607
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { sendDelayMs: 100 }));
6728
7608
  await chmod(join(fakeBinDir, "tmux"), 0o755);
6729
- const stateDir = join(cwd, ".omx", "state");
6730
- const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
6731
- await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
6732
- name: "worker-stop-failed-task",
7609
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7610
+ name: teamName,
6733
7611
  tmux_session: "omx-team-worker-stop",
6734
7612
  leader_pane_id: "%42",
6735
- workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
6736
- });
6737
- await writeJson(join(workerDir, "identity.json"), {
6738
- name: "worker-1",
6739
- assigned_tasks: ["1"],
6740
- team_state_root: stateDir,
6741
- });
6742
- await writeJson(join(workerDir, "status.json"), {
6743
- state: "failed",
6744
- current_task_id: "1",
6745
- updated_at: new Date().toISOString(),
6746
- });
6747
- await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
6748
- id: "1",
6749
- status: "failed",
6750
- owner: "worker-1",
7613
+ workers: [
7614
+ { name: "worker-1", index: 1, pane_id: "%10" },
7615
+ { name: "worker-2", index: 2, pane_id: "%11" },
7616
+ ],
6751
7617
  });
6752
- process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
6753
- delete process.env.OMX_TEAM_INTERNAL_WORKER;
6754
- process.env.OMX_TEAM_STATE_ROOT = stateDir;
7618
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7619
+ const results = await Promise.all([
7620
+ maybeNudgeLeaderForAllowedWorkerStop({
7621
+ stateDir,
7622
+ logsDir,
7623
+ workerContext: { teamName, workerName: "worker-1" },
7624
+ }),
7625
+ maybeNudgeLeaderForAllowedWorkerStop({
7626
+ stateDir,
7627
+ logsDir,
7628
+ workerContext: { teamName, workerName: "worker-2" },
7629
+ }),
7630
+ ]);
7631
+ assert.equal(results.filter((result) => result.result === "sent").length, 1);
7632
+ assert.equal(results.filter((result) => result.result === "suppressed_team_lock_held").length, 1);
7633
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7634
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
7635
+ assert.equal(stopNudges.length, 1, "concurrent same-team workers should emit only one leader nudge");
7636
+ assert.equal(existsSync(join(teamDir, "worker-stop-nudge.lock")), false);
7637
+ }
7638
+ finally {
7639
+ if (typeof prevPath === "string")
7640
+ process.env.PATH = prevPath;
7641
+ else
7642
+ delete process.env.PATH;
7643
+ await rm(cwd, { recursive: true, force: true });
7644
+ }
7645
+ });
7646
+ it("skips worker Stop leader nudge when team state is missing or shut down", async () => {
7647
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-team-"));
7648
+ try {
7649
+ const stateDir = join(cwd, ".omx", "state");
7650
+ const logsDir = join(cwd, ".omx", "logs");
7651
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7652
+ stateDir,
7653
+ logsDir,
7654
+ workerContext: { teamName: "removed-team", workerName: "worker-1" },
7655
+ });
7656
+ assert.equal(result.result, "team_state_gone_or_shutdown");
7657
+ assert.equal(existsSync(join(stateDir, "team", "removed-team", "worker-stop-nudge.json")), false);
7658
+ await writeJson(join(stateDir, "team", "shutdown-team", "shutdown.json"), {
7659
+ started_at: new Date().toISOString(),
7660
+ });
7661
+ const shutdownResult = await maybeNudgeLeaderForAllowedWorkerStop({
7662
+ stateDir,
7663
+ logsDir,
7664
+ workerContext: { teamName: "shutdown-team", workerName: "worker-1" },
7665
+ });
7666
+ assert.equal(shutdownResult.result, "team_state_gone_or_shutdown");
7667
+ assert.equal(existsSync(join(stateDir, "team", "shutdown-team", "worker-stop-nudge.json")), false);
7668
+ }
7669
+ finally {
7670
+ await rm(cwd, { recursive: true, force: true });
7671
+ }
7672
+ });
7673
+ it("does not treat old visible worker Stop transcript as pending queue state", async () => {
7674
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-queue-dedupe-"));
7675
+ const prevPath = process.env.PATH;
7676
+ try {
7677
+ const stateDir = join(cwd, ".omx", "state");
7678
+ const logsDir = join(cwd, ".omx", "logs");
7679
+ const teamName = "queued-stop-dedupe";
7680
+ const teamDir = join(stateDir, "team", teamName);
7681
+ const fakeBinDir = join(cwd, "fake-bin");
7682
+ const tmuxLogPath = join(cwd, "tmux.log");
7683
+ await mkdir(fakeBinDir, { recursive: true });
7684
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
7685
+ busyLeader: true,
7686
+ captureText: `[OMX] worker-1 native Stop allowed. Run \`omx team status ${teamName}\`, read worker messages/results, then assign next task, reconcile completion, or shut down. [OMX_TMUX_INJECT]\n`
7687
+ + "• Working… (esc to interrupt)",
7688
+ }));
7689
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7690
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7691
+ name: teamName,
7692
+ tmux_session: "omx-team-worker-stop",
7693
+ leader_pane_id: "%42",
7694
+ workers: [{ name: "worker-2", index: 2, pane_id: "%11" }],
7695
+ });
7696
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7697
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7698
+ stateDir,
7699
+ logsDir,
7700
+ workerContext: { teamName, workerName: "worker-2" },
7701
+ });
7702
+ assert.equal(result.result, "queued");
7703
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7704
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-2 native Stop allowed/);
7705
+ assert.match(tmuxLog, /send-keys -t %42 Tab/);
7706
+ const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
7707
+ assert.equal(teamNudgeState.worker, "worker-2");
7708
+ assert.equal(teamNudgeState.delivery, "queued");
7709
+ }
7710
+ finally {
7711
+ if (typeof prevPath === "string")
7712
+ process.env.PATH = prevPath;
7713
+ else
7714
+ delete process.env.PATH;
7715
+ await rm(cwd, { recursive: true, force: true });
7716
+ }
7717
+ });
7718
+ it("reports deferred when non-teardown persistence failure prevents worker Stop nudge cooldown state", async () => {
7719
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-persist-fail-"));
7720
+ const prevPath = process.env.PATH;
7721
+ try {
7722
+ const stateDir = join(cwd, ".omx", "state");
7723
+ const logsDir = join(cwd, ".omx", "logs");
7724
+ const teamName = "worker-stop-persist-fail";
7725
+ const teamDir = join(stateDir, "team", teamName);
7726
+ const fakeBinDir = join(cwd, "fake-bin");
7727
+ const tmuxLogPath = join(cwd, "tmux.log");
7728
+ await mkdir(fakeBinDir, { recursive: true });
7729
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7730
+ name: teamName,
7731
+ tmux_session: "omx-team-worker-stop",
7732
+ leader_pane_id: "%42",
7733
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7734
+ });
7735
+ await writeFile(join(teamDir, "workers"), "not a directory");
7736
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
7737
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7738
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7739
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7740
+ stateDir,
7741
+ logsDir,
7742
+ workerContext: { teamName, workerName: "worker-1" },
7743
+ });
7744
+ assert.equal(result.result, "deferred");
7745
+ assert.equal(existsSync(join(teamDir, "worker-stop-nudge.json")), false);
7746
+ assert.equal(existsSync(join(teamDir, "workers", "worker-1", "worker-stop-nudge.json")), false);
7747
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7748
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
7749
+ const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
7750
+ const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
7751
+ .trim()
7752
+ .split("\n")
7753
+ .map((line) => JSON.parse(line));
7754
+ const deferredEvent = deliveryEvents.find((event) => event.event === "nudge_triggered" && event.result === "deferred");
7755
+ assert.equal(deferredEvent?.team, teamName);
7756
+ assert.equal(deferredEvent?.from_worker, "worker-1");
7757
+ assert.match(String(deferredEvent?.reason || ""), /EEXIST|ENOTDIR|not a directory|file already exists/);
7758
+ }
7759
+ finally {
7760
+ if (typeof prevPath === "string")
7761
+ process.env.PATH = prevPath;
7762
+ else
7763
+ delete process.env.PATH;
7764
+ await rm(cwd, { recursive: true, force: true });
7765
+ }
7766
+ });
7767
+ it("does not recreate team state when teardown removes it during worker Stop delivery", async () => {
7768
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-teardown-race-"));
7769
+ const prevPath = process.env.PATH;
7770
+ try {
7771
+ const stateDir = join(cwd, ".omx", "state");
7772
+ const logsDir = join(cwd, ".omx", "logs");
7773
+ const teamName = "worker-stop-teardown-race";
7774
+ const teamDir = join(stateDir, "team", teamName);
7775
+ const fakeBinDir = join(cwd, "fake-bin");
7776
+ const tmuxLogPath = join(cwd, "tmux.log");
7777
+ await mkdir(fakeBinDir, { recursive: true });
7778
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7779
+ name: teamName,
7780
+ tmux_session: "omx-team-worker-stop",
7781
+ leader_pane_id: "%42",
7782
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7783
+ });
7784
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { removePathOnSend: teamDir }));
7785
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7786
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7787
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7788
+ stateDir,
7789
+ logsDir,
7790
+ workerContext: { teamName, workerName: "worker-1" },
7791
+ });
7792
+ assert.equal(result.result, "sent");
7793
+ assert.equal(existsSync(teamDir), false, "worker Stop delivery must not recreate removed team state");
7794
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7795
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
7796
+ }
7797
+ finally {
7798
+ if (typeof prevPath === "string")
7799
+ process.env.PATH = prevPath;
7800
+ else
7801
+ delete process.env.PATH;
7802
+ await rm(cwd, { recursive: true, force: true });
7803
+ }
7804
+ });
7805
+ it("does not recreate team state when teardown removes it before deferred worker Stop recording", async () => {
7806
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-deferred-teardown-"));
7807
+ const prevPath = process.env.PATH;
7808
+ try {
7809
+ const stateDir = join(cwd, ".omx", "state");
7810
+ const logsDir = join(cwd, ".omx", "logs");
7811
+ const teamName = "worker-stop-deferred-teardown";
7812
+ const teamDir = join(stateDir, "team", teamName);
7813
+ const fakeBinDir = join(cwd, "fake-bin");
7814
+ const tmuxLogPath = join(cwd, "tmux.log");
7815
+ await mkdir(fakeBinDir, { recursive: true });
7816
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7817
+ name: teamName,
7818
+ tmux_session: "omx-team-worker-stop",
7819
+ leader_pane_id: "%42",
7820
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7821
+ });
7822
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
7823
+ currentCommand: "bash",
7824
+ captureText: "$ ",
7825
+ removePathOnCapture: teamDir,
7826
+ }));
7827
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7828
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7829
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7830
+ stateDir,
7831
+ logsDir,
7832
+ workerContext: { teamName, workerName: "worker-1" },
7833
+ });
7834
+ assert.equal(result.result, "team_state_gone_or_shutdown");
7835
+ assert.equal(existsSync(teamDir), false, "deferred worker Stop recording must not recreate removed team state");
7836
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7837
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
7838
+ }
7839
+ finally {
7840
+ if (typeof prevPath === "string")
7841
+ process.env.PATH = prevPath;
7842
+ else
7843
+ delete process.env.PATH;
7844
+ await rm(cwd, { recursive: true, force: true });
7845
+ }
7846
+ });
7847
+ it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
7848
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
7849
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
7850
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
7851
+ const prevPath = process.env.PATH;
7852
+ try {
7853
+ await initTeamState("worker-stop-helper-fail", "worker stop helper failure", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" });
7854
+ const fakeBinDir = join(cwd, "fake-bin");
7855
+ await mkdir(fakeBinDir, { recursive: true });
7856
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
7857
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7858
+ const stateDir = join(cwd, ".omx", "state");
7859
+ const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
7860
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
7861
+ name: "worker-stop-helper-fail",
7862
+ tmux_session: "omx-team-worker-stop",
7863
+ leader_pane_id: "%42",
7864
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7865
+ });
7866
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
7867
+ name: "worker-stop-helper-fail",
7868
+ tmux_session: "omx-team-worker-stop",
7869
+ leader_pane_id: "%42",
7870
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7871
+ });
7872
+ await writeJson(join(workerDir, "identity.json"), {
7873
+ name: "worker-1",
7874
+ assigned_tasks: ["1"],
7875
+ team_state_root: stateDir,
7876
+ });
7877
+ await writeJson(join(workerDir, "status.json"), {
7878
+ state: "done",
7879
+ current_task_id: "1",
7880
+ updated_at: new Date().toISOString(),
7881
+ });
7882
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
7883
+ id: "1",
7884
+ status: "completed",
7885
+ owner: "worker-1",
7886
+ });
7887
+ process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
7888
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
7889
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7890
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" }, { cwd });
7891
+ assert.equal(result.outputJson, null);
7892
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
7893
+ assert.equal(nudgeState.delivery, "deferred");
7894
+ }
7895
+ finally {
7896
+ if (typeof prevTeamWorker === "string")
7897
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
7898
+ else
7899
+ delete process.env.OMX_TEAM_WORKER;
7900
+ if (typeof prevTeamStateRoot === "string")
7901
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
7902
+ else
7903
+ delete process.env.OMX_TEAM_STATE_ROOT;
7904
+ if (typeof prevPath === "string")
7905
+ process.env.PATH = prevPath;
7906
+ else
7907
+ delete process.env.PATH;
7908
+ await rm(cwd, { recursive: true, force: true });
7909
+ }
7910
+ });
7911
+ it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
7912
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
7913
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
7914
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
7915
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
7916
+ const prevPath = process.env.PATH;
7917
+ try {
7918
+ await initTeamState("worker-stop-failed-task", "worker stop failed task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" });
7919
+ const fakeBinDir = join(cwd, "fake-bin");
7920
+ const tmuxLogPath = join(cwd, "tmux.log");
7921
+ await mkdir(fakeBinDir, { recursive: true });
7922
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
7923
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7924
+ const stateDir = join(cwd, ".omx", "state");
7925
+ const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
7926
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
7927
+ name: "worker-stop-failed-task",
7928
+ tmux_session: "omx-team-worker-stop",
7929
+ leader_pane_id: "%42",
7930
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7931
+ });
7932
+ await writeJson(join(workerDir, "identity.json"), {
7933
+ name: "worker-1",
7934
+ assigned_tasks: ["1"],
7935
+ team_state_root: stateDir,
7936
+ });
7937
+ await writeJson(join(workerDir, "status.json"), {
7938
+ state: "failed",
7939
+ current_task_id: "1",
7940
+ updated_at: new Date().toISOString(),
7941
+ });
7942
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
7943
+ id: "1",
7944
+ status: "failed",
7945
+ owner: "worker-1",
7946
+ });
7947
+ process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
7948
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
7949
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
6755
7950
  process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
6756
7951
  const result = await dispatchCodexNativeHook({
6757
7952
  hook_event_name: "Stop",
@@ -10150,188 +11345,943 @@ exit 0
10150
11345
  session_id: "sess-stop-team-refire",
10151
11346
  thread_id: "thread-stop-team-refire",
10152
11347
  });
10153
- await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
10154
- current_phase: "team-verify",
10155
- max_fix_attempts: 3,
10156
- current_fix_attempt: 0,
10157
- transitions: [],
10158
- updated_at: new Date().toISOString(),
11348
+ await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
11349
+ current_phase: "team-verify",
11350
+ max_fix_attempts: 3,
11351
+ current_fix_attempt: 0,
11352
+ transitions: [],
11353
+ updated_at: new Date().toISOString(),
11354
+ });
11355
+ await dispatchCodexNativeHook({
11356
+ hook_event_name: "Stop",
11357
+ cwd,
11358
+ session_id: "sess-stop-team-refire",
11359
+ thread_id: "thread-stop-team-refire",
11360
+ turn_id: "turn-stop-team-refire-1",
11361
+ }, { cwd });
11362
+ const result = await dispatchCodexNativeHook({
11363
+ hook_event_name: "Stop",
11364
+ cwd,
11365
+ session_id: "sess-stop-team-refire",
11366
+ thread_id: "thread-stop-team-refire",
11367
+ turn_id: "turn-stop-team-refire-2",
11368
+ stop_hook_active: true,
11369
+ }, { cwd });
11370
+ assert.equal(result.omxEventName, "stop");
11371
+ assert.deepEqual(result.outputJson, {
11372
+ decision: "block",
11373
+ reason: `OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
11374
+ stopReason: "team_team-verify",
11375
+ systemMessage: "OMX team pipeline is still active at phase team-verify.",
11376
+ });
11377
+ }
11378
+ finally {
11379
+ await rm(cwd, { recursive: true, force: true });
11380
+ }
11381
+ });
11382
+ it("suppresses duplicate team Stop replays across native/canonical session-id drift", async () => {
11383
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-drift-"));
11384
+ try {
11385
+ const stateDir = join(cwd, ".omx", "state");
11386
+ await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
11387
+ process.env.OMX_SESSION_ID = "omx-canonical";
11388
+ await writeJson(join(stateDir, "session.json"), {
11389
+ session_id: "omx-canonical",
11390
+ native_session_id: "codex-native",
11391
+ });
11392
+ await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
11393
+ active: true,
11394
+ current_phase: "starting",
11395
+ team_name: "current-team",
11396
+ session_id: "omx-canonical",
11397
+ });
11398
+ await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
11399
+ current_phase: "team-verify",
11400
+ max_fix_attempts: 3,
11401
+ current_fix_attempt: 1,
11402
+ transitions: [],
11403
+ updated_at: new Date().toISOString(),
11404
+ });
11405
+ await dispatchCodexNativeHook({
11406
+ hook_event_name: "Stop",
11407
+ cwd,
11408
+ session_id: "codex-native",
11409
+ thread_id: "thread-stop-team-drift",
11410
+ turn_id: "turn-stop-team-drift-1",
11411
+ }, { cwd });
11412
+ const duplicate = await dispatchCodexNativeHook({
11413
+ hook_event_name: "Stop",
11414
+ cwd,
11415
+ session_id: "omx-canonical",
11416
+ thread_id: "thread-stop-team-drift",
11417
+ turn_id: "turn-stop-team-drift-1",
11418
+ stop_hook_active: true,
11419
+ }, { cwd });
11420
+ assert.equal(duplicate.omxEventName, "stop");
11421
+ assert.deepEqual(duplicate.outputJson, {
11422
+ decision: "block",
11423
+ reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
11424
+ stopReason: "team_team-verify",
11425
+ systemMessage: "OMX team pipeline is still active at phase team-verify.",
11426
+ });
11427
+ const fresh = await dispatchCodexNativeHook({
11428
+ hook_event_name: "Stop",
11429
+ cwd,
11430
+ session_id: "omx-canonical",
11431
+ thread_id: "thread-stop-team-drift",
11432
+ turn_id: "turn-stop-team-drift-2",
11433
+ stop_hook_active: true,
11434
+ }, { cwd });
11435
+ assert.equal(fresh.omxEventName, "stop");
11436
+ assert.deepEqual(fresh.outputJson, {
11437
+ decision: "block",
11438
+ reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
11439
+ stopReason: "team_team-verify",
11440
+ systemMessage: "OMX team pipeline is still active at phase team-verify.",
11441
+ });
11442
+ const persisted = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
11443
+ assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
11444
+ }
11445
+ finally {
11446
+ await rm(cwd, { recursive: true, force: true });
11447
+ }
11448
+ });
11449
+ it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
11450
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
11451
+ try {
11452
+ const stateDir = join(cwd, ".omx", "state");
11453
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
11454
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
11455
+ active: true,
11456
+ current_phase: "executing",
11457
+ });
11458
+ const first = await dispatchCodexNativeHook({
11459
+ hook_event_name: "Stop",
11460
+ cwd,
11461
+ session_id: "sess-stop-ultrawork-repeat",
11462
+ thread_id: "thread-stop-ultrawork-repeat",
11463
+ turn_id: "turn-stop-ultrawork-repeat-1",
11464
+ }, { cwd });
11465
+ const repeated = await dispatchCodexNativeHook({
11466
+ hook_event_name: "Stop",
11467
+ cwd,
11468
+ session_id: "sess-stop-ultrawork-repeat",
11469
+ thread_id: "thread-stop-ultrawork-repeat",
11470
+ turn_id: "turn-stop-ultrawork-repeat-1",
11471
+ stop_hook_active: true,
11472
+ }, { cwd });
11473
+ const fresh = await dispatchCodexNativeHook({
11474
+ hook_event_name: "Stop",
11475
+ cwd,
11476
+ session_id: "sess-stop-ultrawork-repeat",
11477
+ thread_id: "thread-stop-ultrawork-repeat",
11478
+ turn_id: "turn-stop-ultrawork-repeat-2",
11479
+ stop_hook_active: true,
11480
+ }, { cwd });
11481
+ assert.equal(first.omxEventName, "stop");
11482
+ assert.deepEqual(repeated.outputJson, null);
11483
+ assert.equal(fresh.omxEventName, "stop");
11484
+ assert.deepEqual(fresh.outputJson, {
11485
+ decision: "block",
11486
+ reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
11487
+ stopReason: "ultrawork_executing",
11488
+ systemMessage: "OMX ultrawork is still active (phase: executing).",
11489
+ });
11490
+ }
11491
+ finally {
11492
+ await rm(cwd, { recursive: true, force: true });
11493
+ }
11494
+ });
11495
+ it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
11496
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
11497
+ try {
11498
+ const stateDir = join(cwd, ".omx", "state");
11499
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
11500
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
11501
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
11502
+ active: true,
11503
+ skill: "ralplan",
11504
+ phase: "planning",
11505
+ });
11506
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
11507
+ active: true,
11508
+ current_phase: "planning",
11509
+ });
11510
+ await dispatchCodexNativeHook({
11511
+ hook_event_name: "Stop",
11512
+ cwd,
11513
+ session_id: "sess-stop-skill-repeat",
11514
+ thread_id: "thread-stop-skill-repeat",
11515
+ turn_id: "turn-stop-skill-repeat-1",
11516
+ }, { cwd });
11517
+ const repeated = await dispatchCodexNativeHook({
11518
+ hook_event_name: "Stop",
11519
+ cwd,
11520
+ session_id: "sess-stop-skill-repeat",
11521
+ thread_id: "thread-stop-skill-repeat",
11522
+ turn_id: "turn-stop-skill-repeat-1",
11523
+ stop_hook_active: true,
11524
+ }, { cwd });
11525
+ assert.equal(repeated.omxEventName, "stop");
11526
+ assert.equal(repeated.outputJson?.decision, "block");
11527
+ assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
11528
+ assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
11529
+ assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
11530
+ }
11531
+ finally {
11532
+ await rm(cwd, { recursive: true, force: true });
11533
+ }
11534
+ });
11535
+ it("blocks implementation writes while ralplan is active without execution handoff", async () => {
11536
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-block-"));
11537
+ try {
11538
+ const stateDir = join(cwd, ".omx", "state");
11539
+ const sessionId = "sess-ralplan-pretool-block";
11540
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11541
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11542
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11543
+ active: true,
11544
+ skill: "ralplan",
11545
+ phase: "planning",
11546
+ session_id: sessionId,
11547
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
11548
+ });
11549
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11550
+ active: true,
11551
+ mode: "ralplan",
11552
+ current_phase: "critic-review",
11553
+ session_id: sessionId,
11554
+ });
11555
+ const result = await dispatchCodexNativeHook({
11556
+ hook_event_name: "PreToolUse",
11557
+ cwd,
11558
+ session_id: sessionId,
11559
+ thread_id: "thread-ralplan-pretool-block",
11560
+ tool_name: "Edit",
11561
+ tool_input: { file_path: "src/runtime.ts" },
11562
+ }, { cwd });
11563
+ assert.equal(result.omxEventName, "pre-tool-use");
11564
+ assert.equal(result.outputJson?.decision, "block");
11565
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11566
+ assert.match(String(result.outputJson?.hookSpecificOutput?.additionalContext ?? ""), /\$ultragoal.*\$team.*\$ralph/i);
11567
+ }
11568
+ finally {
11569
+ await rm(cwd, { recursive: true, force: true });
11570
+ }
11571
+ });
11572
+ it("blocks implementation writes while Autopilot is supervising ralplan without handoff", async () => {
11573
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-block-"));
11574
+ try {
11575
+ const stateDir = join(cwd, ".omx", "state");
11576
+ const sessionId = "sess-autopilot-ralplan-pretool-block";
11577
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11578
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11579
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11580
+ active: true,
11581
+ skill: "autopilot",
11582
+ phase: "ralplan",
11583
+ session_id: sessionId,
11584
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
11585
+ });
11586
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11587
+ active: true,
11588
+ mode: "autopilot",
11589
+ current_phase: "ralplan",
11590
+ session_id: sessionId,
11591
+ state: {
11592
+ handoff_artifacts: {
11593
+ ralplan_consensus_gate: { required: true, complete: false },
11594
+ },
11595
+ },
11596
+ });
11597
+ const result = await dispatchCodexNativeHook({
11598
+ hook_event_name: "PreToolUse",
11599
+ cwd,
11600
+ session_id: sessionId,
11601
+ thread_id: "thread-autopilot-ralplan-pretool-block",
11602
+ tool_name: "Edit",
11603
+ tool_input: { file_path: "src/runtime.ts" },
11604
+ }, { cwd });
11605
+ assert.equal(result.omxEventName, "pre-tool-use");
11606
+ assert.equal(result.outputJson?.decision, "block");
11607
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11608
+ }
11609
+ finally {
11610
+ await rm(cwd, { recursive: true, force: true });
11611
+ }
11612
+ });
11613
+ it("does not block implementation writes from Autopilot ralplan detail state without canonical skill state", async () => {
11614
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-no-canonical-"));
11615
+ try {
11616
+ const stateDir = join(cwd, ".omx", "state");
11617
+ const sessionId = "sess-autopilot-ralplan-no-canonical";
11618
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11619
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11620
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11621
+ active: true,
11622
+ mode: "autopilot",
11623
+ current_phase: "ralplan",
11624
+ session_id: sessionId,
11625
+ });
11626
+ const result = await dispatchCodexNativeHook({
11627
+ hook_event_name: "PreToolUse",
11628
+ cwd,
11629
+ session_id: sessionId,
11630
+ thread_id: "thread-autopilot-ralplan-no-canonical",
11631
+ tool_name: "Edit",
11632
+ tool_input: { file_path: "src/runtime.ts" },
11633
+ }, { cwd });
11634
+ assert.equal(result.omxEventName, "pre-tool-use");
11635
+ assert.equal(result.outputJson, null);
11636
+ }
11637
+ finally {
11638
+ await rm(cwd, { recursive: true, force: true });
11639
+ }
11640
+ });
11641
+ it("allows implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
11642
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-terminal-pretool-"));
11643
+ try {
11644
+ const stateDir = join(cwd, ".omx", "state");
11645
+ const sessionId = "sess-autopilot-ralplan-terminal-pretool";
11646
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11647
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11648
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11649
+ active: true,
11650
+ skill: "autopilot",
11651
+ phase: "ralplan",
11652
+ session_id: sessionId,
11653
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
11654
+ });
11655
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11656
+ active: true,
11657
+ mode: "autopilot",
11658
+ current_phase: "ralplan",
11659
+ session_id: sessionId,
11660
+ });
11661
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
11662
+ version: 1,
11663
+ active: false,
11664
+ mode: "autopilot",
11665
+ outcome: "finish",
11666
+ lifecycle_outcome: "finished",
11667
+ current_phase: "complete",
11668
+ completed_at: "2026-05-30T00:00:00.000Z",
11669
+ updated_at: "2026-05-30T00:00:00.000Z",
11670
+ });
11671
+ const result = await dispatchCodexNativeHook({
11672
+ hook_event_name: "PreToolUse",
11673
+ cwd,
11674
+ session_id: sessionId,
11675
+ thread_id: "thread-autopilot-ralplan-terminal-pretool",
11676
+ tool_name: "Edit",
11677
+ tool_input: { file_path: "src/runtime.ts" },
11678
+ }, { cwd });
11679
+ assert.equal(result.omxEventName, "pre-tool-use");
11680
+ assert.equal(result.outputJson, null);
11681
+ }
11682
+ finally {
11683
+ await rm(cwd, { recursive: true, force: true });
11684
+ }
11685
+ });
11686
+ it("blocks bash implementation writes while Autopilot is supervising ralplan without handoff", async () => {
11687
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-bash-block-"));
11688
+ try {
11689
+ const stateDir = join(cwd, ".omx", "state");
11690
+ const sessionId = "sess-autopilot-ralplan-pretool-bash-block";
11691
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11692
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11693
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11694
+ active: true,
11695
+ skill: "autopilot",
11696
+ phase: "ralplan",
11697
+ session_id: sessionId,
11698
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
11699
+ });
11700
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11701
+ active: true,
11702
+ mode: "autopilot",
11703
+ current_phase: "ralplan",
11704
+ session_id: sessionId,
11705
+ });
11706
+ const result = await dispatchCodexNativeHook({
11707
+ hook_event_name: "PreToolUse",
11708
+ cwd,
11709
+ session_id: sessionId,
11710
+ thread_id: "thread-autopilot-ralplan-pretool-bash-block",
11711
+ tool_name: "Bash",
11712
+ tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
11713
+ }, { cwd });
11714
+ assert.equal(result.omxEventName, "pre-tool-use");
11715
+ assert.equal(result.outputJson?.decision, "block");
11716
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11717
+ }
11718
+ finally {
11719
+ await rm(cwd, { recursive: true, force: true });
11720
+ }
11721
+ });
11722
+ it("blocks implementation writes when ralplan and Autopilot ralplan are both active", async () => {
11723
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-autopilot-mixed-planning-"));
11724
+ try {
11725
+ const stateDir = join(cwd, ".omx", "state");
11726
+ const sessionId = "sess-ralplan-autopilot-mixed-planning";
11727
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11728
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11729
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11730
+ active: true,
11731
+ skill: "autopilot",
11732
+ phase: "ralplan",
11733
+ session_id: sessionId,
11734
+ active_skills: [
11735
+ { skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
11736
+ { skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId },
11737
+ ],
11738
+ });
11739
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11740
+ active: true,
11741
+ mode: "ralplan",
11742
+ current_phase: "planning",
11743
+ session_id: sessionId,
11744
+ });
11745
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11746
+ active: true,
11747
+ mode: "autopilot",
11748
+ current_phase: "ralplan",
11749
+ session_id: sessionId,
11750
+ });
11751
+ const result = await dispatchCodexNativeHook({
11752
+ hook_event_name: "PreToolUse",
11753
+ cwd,
11754
+ session_id: sessionId,
11755
+ thread_id: "thread-ralplan-autopilot-mixed-planning",
11756
+ tool_name: "Edit",
11757
+ tool_input: { file_path: "src/runtime.ts" },
11758
+ }, { cwd });
11759
+ assert.equal(result.omxEventName, "pre-tool-use");
11760
+ assert.equal(result.outputJson?.decision, "block");
11761
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11762
+ }
11763
+ finally {
11764
+ await rm(cwd, { recursive: true, force: true });
11765
+ }
11766
+ });
11767
+ it("blocks implementation writes while Autopilot is supervising replan without handoff", async () => {
11768
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-replan-pretool-block-"));
11769
+ try {
11770
+ const stateDir = join(cwd, ".omx", "state");
11771
+ const sessionId = "sess-autopilot-replan-pretool-block";
11772
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11773
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11774
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11775
+ active: true,
11776
+ skill: "autopilot",
11777
+ phase: "replan",
11778
+ session_id: sessionId,
11779
+ active_skills: [{ skill: "autopilot", phase: "replan", active: true, session_id: sessionId }],
11780
+ });
11781
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11782
+ active: true,
11783
+ mode: "autopilot",
11784
+ current_phase: "replan",
11785
+ session_id: sessionId,
11786
+ });
11787
+ const result = await dispatchCodexNativeHook({
11788
+ hook_event_name: "PreToolUse",
11789
+ cwd,
11790
+ session_id: sessionId,
11791
+ thread_id: "thread-autopilot-replan-pretool-block",
11792
+ tool_name: "Edit",
11793
+ tool_input: { file_path: "src/runtime.ts" },
11794
+ }, { cwd });
11795
+ assert.equal(result.omxEventName, "pre-tool-use");
11796
+ assert.equal(result.outputJson?.decision, "block");
11797
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11798
+ }
11799
+ finally {
11800
+ await rm(cwd, { recursive: true, force: true });
11801
+ }
11802
+ });
11803
+ it("blocks implementation writes when native Codex id maps to OMX Autopilot ralplan state", async () => {
11804
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-native-map-block-"));
11805
+ try {
11806
+ const stateDir = join(cwd, ".omx", "state");
11807
+ const sessionId = "sess-autopilot-ralplan-native-map-block";
11808
+ const nativeSessionId = "019e-autopilot-ralplan-native";
11809
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
11810
+ await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
11811
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11812
+ active: true,
11813
+ mode: "autopilot",
11814
+ current_phase: "ralplan",
11815
+ session_id: sessionId,
11816
+ });
11817
+ const result = await dispatchCodexNativeHook({
11818
+ hook_event_name: "PreToolUse",
11819
+ cwd,
11820
+ session_id: nativeSessionId,
11821
+ thread_id: "thread-autopilot-ralplan-native-map-block",
11822
+ tool_name: "apply_patch",
11823
+ tool_input: { file_path: "src/runtime.ts" },
11824
+ }, { cwd });
11825
+ assert.equal(result.omxEventName, "pre-tool-use");
11826
+ assert.equal(result.outputJson?.decision, "block");
11827
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11828
+ }
11829
+ finally {
11830
+ await rm(cwd, { recursive: true, force: true });
11831
+ }
11832
+ });
11833
+ it("blocks bash implementation writes when native Codex id maps to OMX Autopilot ralplan state", async () => {
11834
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-native-map-bash-"));
11835
+ try {
11836
+ const stateDir = join(cwd, ".omx", "state");
11837
+ const sessionId = "sess-autopilot-ralplan-native-map-bash";
11838
+ const nativeSessionId = "019e-autopilot-ralplan-native-bash";
11839
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
11840
+ await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
11841
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11842
+ active: true,
11843
+ mode: "autopilot",
11844
+ current_phase: "ralplan",
11845
+ session_id: sessionId,
11846
+ });
11847
+ const result = await dispatchCodexNativeHook({
11848
+ hook_event_name: "PreToolUse",
11849
+ cwd,
11850
+ session_id: nativeSessionId,
11851
+ thread_id: "thread-autopilot-ralplan-native-map-bash",
11852
+ tool_name: "Bash",
11853
+ tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
11854
+ }, { cwd });
11855
+ assert.equal(result.omxEventName, "pre-tool-use");
11856
+ assert.equal(result.outputJson?.decision, "block");
11857
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11858
+ }
11859
+ finally {
11860
+ await rm(cwd, { recursive: true, force: true });
11861
+ }
11862
+ });
11863
+ it("blocks standalone ralplan writes when native Codex id maps to OMX session state", async () => {
11864
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-block-"));
11865
+ try {
11866
+ const stateDir = join(cwd, ".omx", "state");
11867
+ const sessionId = "sess-ralplan-native-map-block";
11868
+ const nativeSessionId = "019e-ralplan-native-map";
11869
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
11870
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
11871
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11872
+ active: true,
11873
+ mode: "ralplan",
11874
+ current_phase: "planning",
11875
+ session_id: sessionId,
11876
+ });
11877
+ const result = await dispatchCodexNativeHook({
11878
+ hook_event_name: "PreToolUse",
11879
+ cwd,
11880
+ session_id: nativeSessionId,
11881
+ thread_id: "thread-ralplan-native-map-block",
11882
+ tool_name: "Edit",
11883
+ tool_input: { file_path: "src/runtime.ts" },
11884
+ }, { cwd });
11885
+ assert.equal(result.omxEventName, "pre-tool-use");
11886
+ assert.equal(result.outputJson?.decision, "block");
11887
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
11888
+ }
11889
+ finally {
11890
+ await rm(cwd, { recursive: true, force: true });
11891
+ }
11892
+ });
11893
+ it("blocks deep-interview writes when native Codex id maps to OMX session state", async () => {
11894
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-native-map-block-"));
11895
+ try {
11896
+ const stateDir = join(cwd, ".omx", "state");
11897
+ const sessionId = "sess-deep-interview-native-map-block";
11898
+ const nativeSessionId = "019e-deep-interview-native-map";
11899
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
11900
+ await writeSessionSkillActiveState(stateDir, sessionId, "deep-interview", "interview");
11901
+ await writeJson(join(stateDir, "sessions", sessionId, "deep-interview-state.json"), {
11902
+ active: true,
11903
+ mode: "deep-interview",
11904
+ current_phase: "interview",
11905
+ session_id: sessionId,
11906
+ });
11907
+ const result = await dispatchCodexNativeHook({
11908
+ hook_event_name: "PreToolUse",
11909
+ cwd,
11910
+ session_id: nativeSessionId,
11911
+ thread_id: "thread-deep-interview-native-map-block",
11912
+ tool_name: "Edit",
11913
+ tool_input: { file_path: "src/runtime.ts" },
11914
+ }, { cwd });
11915
+ assert.equal(result.omxEventName, "pre-tool-use");
11916
+ assert.equal(result.outputJson?.decision, "block");
11917
+ assert.match(String(result.outputJson?.reason ?? ""), /Deep-interview is active .*implementation\/write tools are blocked/i);
11918
+ }
11919
+ finally {
11920
+ await rm(cwd, { recursive: true, force: true });
11921
+ }
11922
+ });
11923
+ it("allows mapped ralplan planning artifact writes without execution handoff", async () => {
11924
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-artifact-"));
11925
+ try {
11926
+ const stateDir = join(cwd, ".omx", "state");
11927
+ const sessionId = "sess-ralplan-native-map-artifact";
11928
+ const nativeSessionId = "019e-ralplan-native-map-artifact";
11929
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
11930
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
11931
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11932
+ active: true,
11933
+ mode: "ralplan",
11934
+ current_phase: "planning",
11935
+ session_id: sessionId,
11936
+ });
11937
+ const result = await dispatchCodexNativeHook({
11938
+ hook_event_name: "PreToolUse",
11939
+ cwd,
11940
+ session_id: nativeSessionId,
11941
+ thread_id: "thread-ralplan-native-map-artifact",
11942
+ tool_name: "Bash",
11943
+ tool_input: { command: "cat <<'EOF' > .omx/plans/prd-native-map.md\nplanning\nEOF" },
11944
+ }, { cwd });
11945
+ assert.equal(result.omxEventName, "pre-tool-use");
11946
+ assert.equal(result.outputJson, null);
11947
+ }
11948
+ finally {
11949
+ await rm(cwd, { recursive: true, force: true });
11950
+ }
11951
+ });
11952
+ it("allows mapped implementation writes when explicit execution handoff is active", async () => {
11953
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-handoff-"));
11954
+ try {
11955
+ const stateDir = join(cwd, ".omx", "state");
11956
+ const sessionId = "sess-ralplan-native-map-handoff";
11957
+ const nativeSessionId = "019e-ralplan-native-map-handoff";
11958
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
11959
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11960
+ active: true,
11961
+ skill: "ultragoal",
11962
+ phase: "planning",
11963
+ session_id: sessionId,
11964
+ active_skills: [
11965
+ { skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
11966
+ { skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
11967
+ ],
11968
+ });
11969
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11970
+ active: true,
11971
+ mode: "ralplan",
11972
+ current_phase: "complete",
11973
+ session_id: sessionId,
11974
+ });
11975
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
11976
+ active: true,
11977
+ mode: "ultragoal",
11978
+ current_phase: "planning",
11979
+ session_id: sessionId,
11980
+ });
11981
+ const result = await dispatchCodexNativeHook({
11982
+ hook_event_name: "PreToolUse",
11983
+ cwd,
11984
+ session_id: nativeSessionId,
11985
+ thread_id: "thread-ralplan-native-map-handoff",
11986
+ tool_name: "Edit",
11987
+ tool_input: { file_path: "src/runtime.ts" },
11988
+ }, { cwd });
11989
+ assert.equal(result.omxEventName, "pre-tool-use");
11990
+ assert.equal(result.outputJson, null);
11991
+ }
11992
+ finally {
11993
+ await rm(cwd, { recursive: true, force: true });
11994
+ }
11995
+ });
11996
+ it("allows mapped implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
11997
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-native-map-terminal-"));
11998
+ try {
11999
+ const stateDir = join(cwd, ".omx", "state");
12000
+ const sessionId = "sess-autopilot-ralplan-native-map-terminal";
12001
+ const nativeSessionId = "019e-autopilot-ralplan-native-terminal";
12002
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
12003
+ await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
12004
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
12005
+ active: true,
12006
+ mode: "autopilot",
12007
+ current_phase: "ralplan",
12008
+ session_id: sessionId,
12009
+ });
12010
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
12011
+ version: 1,
12012
+ active: false,
12013
+ mode: "autopilot",
12014
+ outcome: "finish",
12015
+ lifecycle_outcome: "finished",
12016
+ current_phase: "complete",
12017
+ completed_at: "2026-05-30T00:00:00.000Z",
12018
+ updated_at: "2026-05-30T00:00:00.000Z",
12019
+ });
12020
+ const result = await dispatchCodexNativeHook({
12021
+ hook_event_name: "PreToolUse",
12022
+ cwd,
12023
+ session_id: nativeSessionId,
12024
+ thread_id: "thread-autopilot-ralplan-native-map-terminal",
12025
+ tool_name: "Edit",
12026
+ tool_input: { file_path: "src/runtime.ts" },
12027
+ }, { cwd });
12028
+ assert.equal(result.omxEventName, "pre-tool-use");
12029
+ assert.equal(result.outputJson, null);
12030
+ }
12031
+ finally {
12032
+ await rm(cwd, { recursive: true, force: true });
12033
+ }
12034
+ });
12035
+ it("does not block unrelated native Codex ids when current OMX session mapping does not match", async () => {
12036
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-unrelated-"));
12037
+ try {
12038
+ const stateDir = join(cwd, ".omx", "state");
12039
+ const sessionId = "sess-ralplan-native-map-owner";
12040
+ const ownerNativeSessionId = "019e-ralplan-native-owner";
12041
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, ownerNativeSessionId);
12042
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
12043
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12044
+ active: true,
12045
+ mode: "ralplan",
12046
+ current_phase: "planning",
12047
+ session_id: sessionId,
12048
+ });
12049
+ const result = await dispatchCodexNativeHook({
12050
+ hook_event_name: "PreToolUse",
12051
+ cwd,
12052
+ session_id: "019e-unrelated-native-session",
12053
+ thread_id: "thread-ralplan-native-map-unrelated",
12054
+ tool_name: "Edit",
12055
+ tool_input: { file_path: "src/runtime.ts" },
12056
+ }, { cwd });
12057
+ assert.equal(result.omxEventName, "pre-tool-use");
12058
+ assert.equal(result.outputJson, null);
12059
+ }
12060
+ finally {
12061
+ await rm(cwd, { recursive: true, force: true });
12062
+ }
12063
+ });
12064
+ it("blocks mapped Autopilot ralplan writes from the authoritative team state root", async () => {
12065
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-team-root-"));
12066
+ const teamStateRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-team-root-"));
12067
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
12068
+ try {
12069
+ process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
12070
+ const stateDir = teamStateRoot;
12071
+ const sessionId = "sess-autopilot-ralplan-team-root";
12072
+ const nativeSessionId = "019e-autopilot-ralplan-team-root";
12073
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
12074
+ await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
12075
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
12076
+ active: true,
12077
+ mode: "autopilot",
12078
+ current_phase: "ralplan",
12079
+ session_id: sessionId,
12080
+ });
12081
+ const result = await dispatchCodexNativeHook({
12082
+ hook_event_name: "PreToolUse",
12083
+ cwd,
12084
+ session_id: nativeSessionId,
12085
+ thread_id: "thread-autopilot-ralplan-team-root",
12086
+ tool_name: "Edit",
12087
+ tool_input: { file_path: "src/runtime.ts" },
12088
+ }, { cwd });
12089
+ assert.equal(result.omxEventName, "pre-tool-use");
12090
+ assert.equal(result.outputJson?.decision, "block");
12091
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
12092
+ assert.equal(existsSync(join(cwd, ".omx", "state", "session.json")), false);
12093
+ }
12094
+ finally {
12095
+ if (typeof previousTeamStateRoot === "string")
12096
+ process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
12097
+ else
12098
+ delete process.env.OMX_TEAM_STATE_ROOT;
12099
+ await rm(cwd, { recursive: true, force: true });
12100
+ await rm(teamStateRoot, { recursive: true, force: true });
12101
+ }
12102
+ });
12103
+ it("does not block unrelated native Codex ids from the authoritative team state root", async () => {
12104
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-team-root-unrelated-"));
12105
+ const teamStateRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-team-root-unrelated-"));
12106
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
12107
+ try {
12108
+ process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
12109
+ const stateDir = teamStateRoot;
12110
+ const sessionId = "sess-ralplan-team-root-owner";
12111
+ const nativeSessionId = "019e-ralplan-team-root-owner";
12112
+ await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
12113
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
12114
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12115
+ active: true,
12116
+ mode: "ralplan",
12117
+ current_phase: "planning",
12118
+ session_id: sessionId,
12119
+ });
12120
+ const result = await dispatchCodexNativeHook({
12121
+ hook_event_name: "PreToolUse",
12122
+ cwd,
12123
+ session_id: "019e-unrelated-team-root-native",
12124
+ thread_id: "thread-ralplan-team-root-unrelated",
12125
+ tool_name: "Edit",
12126
+ tool_input: { file_path: "src/runtime.ts" },
12127
+ }, { cwd });
12128
+ assert.equal(result.omxEventName, "pre-tool-use");
12129
+ assert.equal(result.outputJson, null);
12130
+ }
12131
+ finally {
12132
+ if (typeof previousTeamStateRoot === "string")
12133
+ process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
12134
+ else
12135
+ delete process.env.OMX_TEAM_STATE_ROOT;
12136
+ await rm(cwd, { recursive: true, force: true });
12137
+ await rm(teamStateRoot, { recursive: true, force: true });
12138
+ }
12139
+ });
12140
+ it("allows ralplan planning artifact writes without execution handoff", async () => {
12141
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-artifact-"));
12142
+ try {
12143
+ const stateDir = join(cwd, ".omx", "state");
12144
+ const sessionId = "sess-ralplan-pretool-artifact";
12145
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12146
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12147
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
12148
+ active: true,
12149
+ skill: "ralplan",
12150
+ phase: "planning",
12151
+ session_id: sessionId,
12152
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
12153
+ });
12154
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12155
+ active: true,
12156
+ mode: "ralplan",
12157
+ current_phase: "planning",
12158
+ session_id: sessionId,
10159
12159
  });
10160
- await dispatchCodexNativeHook({
10161
- hook_event_name: "Stop",
10162
- cwd,
10163
- session_id: "sess-stop-team-refire",
10164
- thread_id: "thread-stop-team-refire",
10165
- turn_id: "turn-stop-team-refire-1",
10166
- }, { cwd });
10167
12160
  const result = await dispatchCodexNativeHook({
10168
- hook_event_name: "Stop",
12161
+ hook_event_name: "PreToolUse",
10169
12162
  cwd,
10170
- session_id: "sess-stop-team-refire",
10171
- thread_id: "thread-stop-team-refire",
10172
- turn_id: "turn-stop-team-refire-2",
10173
- stop_hook_active: true,
12163
+ session_id: sessionId,
12164
+ thread_id: "thread-ralplan-pretool-artifact",
12165
+ tool_name: "Write",
12166
+ tool_input: { file_path: ".omx/plans/prd-issue-2603.md" },
10174
12167
  }, { cwd });
10175
- assert.equal(result.omxEventName, "stop");
10176
- assert.deepEqual(result.outputJson, {
10177
- decision: "block",
10178
- reason: `OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
10179
- stopReason: "team_team-verify",
10180
- systemMessage: "OMX team pipeline is still active at phase team-verify.",
10181
- });
12168
+ assert.equal(result.omxEventName, "pre-tool-use");
12169
+ assert.equal(result.outputJson, null);
10182
12170
  }
10183
12171
  finally {
10184
12172
  await rm(cwd, { recursive: true, force: true });
10185
12173
  }
10186
12174
  });
10187
- it("suppresses duplicate team Stop replays across native/canonical session-id drift", async () => {
10188
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-drift-"));
12175
+ it("blocks bash implementation writes while ralplan is active without execution handoff", async () => {
12176
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-block-"));
10189
12177
  try {
10190
12178
  const stateDir = join(cwd, ".omx", "state");
10191
- await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
10192
- process.env.OMX_SESSION_ID = "omx-canonical";
10193
- await writeJson(join(stateDir, "session.json"), {
10194
- session_id: "omx-canonical",
10195
- native_session_id: "codex-native",
10196
- });
10197
- await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
12179
+ const sessionId = "sess-ralplan-pretool-bash-block";
12180
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12181
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12182
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
10198
12183
  active: true,
10199
- current_phase: "starting",
10200
- team_name: "current-team",
10201
- session_id: "omx-canonical",
10202
- });
10203
- await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
10204
- current_phase: "team-verify",
10205
- max_fix_attempts: 3,
10206
- current_fix_attempt: 1,
10207
- transitions: [],
10208
- updated_at: new Date().toISOString(),
12184
+ skill: "ralplan",
12185
+ phase: "planning",
12186
+ session_id: sessionId,
12187
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
10209
12188
  });
10210
- await dispatchCodexNativeHook({
10211
- hook_event_name: "Stop",
10212
- cwd,
10213
- session_id: "codex-native",
10214
- thread_id: "thread-stop-team-drift",
10215
- turn_id: "turn-stop-team-drift-1",
10216
- }, { cwd });
10217
- const duplicate = await dispatchCodexNativeHook({
10218
- hook_event_name: "Stop",
10219
- cwd,
10220
- session_id: "omx-canonical",
10221
- thread_id: "thread-stop-team-drift",
10222
- turn_id: "turn-stop-team-drift-1",
10223
- stop_hook_active: true,
10224
- }, { cwd });
10225
- assert.equal(duplicate.omxEventName, "stop");
10226
- assert.deepEqual(duplicate.outputJson, {
10227
- decision: "block",
10228
- reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
10229
- stopReason: "team_team-verify",
10230
- systemMessage: "OMX team pipeline is still active at phase team-verify.",
12189
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12190
+ active: true,
12191
+ mode: "ralplan",
12192
+ current_phase: "planning",
12193
+ session_id: sessionId,
10231
12194
  });
10232
- const fresh = await dispatchCodexNativeHook({
10233
- hook_event_name: "Stop",
12195
+ const result = await dispatchCodexNativeHook({
12196
+ hook_event_name: "PreToolUse",
10234
12197
  cwd,
10235
- session_id: "omx-canonical",
10236
- thread_id: "thread-stop-team-drift",
10237
- turn_id: "turn-stop-team-drift-2",
10238
- stop_hook_active: true,
12198
+ session_id: sessionId,
12199
+ thread_id: "thread-ralplan-pretool-bash-block",
12200
+ tool_name: "Bash",
12201
+ tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
10239
12202
  }, { cwd });
10240
- assert.equal(fresh.omxEventName, "stop");
10241
- assert.deepEqual(fresh.outputJson, {
10242
- decision: "block",
10243
- reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
10244
- stopReason: "team_team-verify",
10245
- systemMessage: "OMX team pipeline is still active at phase team-verify.",
10246
- });
10247
- const persisted = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
10248
- assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
12203
+ assert.equal(result.omxEventName, "pre-tool-use");
12204
+ assert.equal(result.outputJson?.decision, "block");
12205
+ assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
10249
12206
  }
10250
12207
  finally {
10251
12208
  await rm(cwd, { recursive: true, force: true });
10252
12209
  }
10253
12210
  });
10254
- it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
10255
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
12211
+ it("allows bash planning artifact writes while ralplan is active without execution handoff", async () => {
12212
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-artifact-"));
10256
12213
  try {
10257
12214
  const stateDir = join(cwd, ".omx", "state");
10258
- await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
10259
- await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
12215
+ const sessionId = "sess-ralplan-pretool-bash-artifact";
12216
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12217
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12218
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
10260
12219
  active: true,
10261
- current_phase: "executing",
12220
+ skill: "ralplan",
12221
+ phase: "planning",
12222
+ session_id: sessionId,
12223
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
10262
12224
  });
10263
- const first = await dispatchCodexNativeHook({
10264
- hook_event_name: "Stop",
10265
- cwd,
10266
- session_id: "sess-stop-ultrawork-repeat",
10267
- thread_id: "thread-stop-ultrawork-repeat",
10268
- turn_id: "turn-stop-ultrawork-repeat-1",
10269
- }, { cwd });
10270
- const repeated = await dispatchCodexNativeHook({
10271
- hook_event_name: "Stop",
10272
- cwd,
10273
- session_id: "sess-stop-ultrawork-repeat",
10274
- thread_id: "thread-stop-ultrawork-repeat",
10275
- turn_id: "turn-stop-ultrawork-repeat-1",
10276
- stop_hook_active: true,
10277
- }, { cwd });
10278
- const fresh = await dispatchCodexNativeHook({
10279
- hook_event_name: "Stop",
12225
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12226
+ active: true,
12227
+ mode: "ralplan",
12228
+ current_phase: "planning",
12229
+ session_id: sessionId,
12230
+ });
12231
+ const result = await dispatchCodexNativeHook({
12232
+ hook_event_name: "PreToolUse",
10280
12233
  cwd,
10281
- session_id: "sess-stop-ultrawork-repeat",
10282
- thread_id: "thread-stop-ultrawork-repeat",
10283
- turn_id: "turn-stop-ultrawork-repeat-2",
10284
- stop_hook_active: true,
12234
+ session_id: sessionId,
12235
+ thread_id: "thread-ralplan-pretool-bash-artifact",
12236
+ tool_name: "Bash",
12237
+ tool_input: { command: "cat <<'EOF' > .omx/plans/prd-issue-2603.md\nplanning\nEOF" },
10285
12238
  }, { cwd });
10286
- assert.equal(first.omxEventName, "stop");
10287
- assert.deepEqual(repeated.outputJson, null);
10288
- assert.equal(fresh.omxEventName, "stop");
10289
- assert.deepEqual(fresh.outputJson, {
10290
- decision: "block",
10291
- reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
10292
- stopReason: "ultrawork_executing",
10293
- systemMessage: "OMX ultrawork is still active (phase: executing).",
10294
- });
12239
+ assert.equal(result.omxEventName, "pre-tool-use");
12240
+ assert.equal(result.outputJson, null);
10295
12241
  }
10296
12242
  finally {
10297
12243
  await rm(cwd, { recursive: true, force: true });
10298
12244
  }
10299
12245
  });
10300
- it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
10301
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
12246
+ it("allows implementation writes when an explicit execution handoff is active", async () => {
12247
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-handoff-"));
10302
12248
  try {
10303
12249
  const stateDir = join(cwd, ".omx", "state");
10304
- await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
10305
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
10306
- await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
12250
+ const sessionId = "sess-ralplan-pretool-handoff";
12251
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12252
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12253
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
10307
12254
  active: true,
10308
- skill: "ralplan",
12255
+ skill: "ultragoal",
10309
12256
  phase: "planning",
12257
+ session_id: sessionId,
12258
+ active_skills: [
12259
+ { skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
12260
+ { skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
12261
+ ],
10310
12262
  });
10311
- await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
12263
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
10312
12264
  active: true,
12265
+ mode: "ralplan",
12266
+ current_phase: "complete",
12267
+ session_id: sessionId,
12268
+ });
12269
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
12270
+ active: true,
12271
+ mode: "ultragoal",
10313
12272
  current_phase: "planning",
12273
+ session_id: sessionId,
10314
12274
  });
10315
- await dispatchCodexNativeHook({
10316
- hook_event_name: "Stop",
10317
- cwd,
10318
- session_id: "sess-stop-skill-repeat",
10319
- thread_id: "thread-stop-skill-repeat",
10320
- turn_id: "turn-stop-skill-repeat-1",
10321
- }, { cwd });
10322
- const repeated = await dispatchCodexNativeHook({
10323
- hook_event_name: "Stop",
12275
+ const result = await dispatchCodexNativeHook({
12276
+ hook_event_name: "PreToolUse",
10324
12277
  cwd,
10325
- session_id: "sess-stop-skill-repeat",
10326
- thread_id: "thread-stop-skill-repeat",
10327
- turn_id: "turn-stop-skill-repeat-1",
10328
- stop_hook_active: true,
12278
+ session_id: sessionId,
12279
+ thread_id: "thread-ralplan-pretool-handoff",
12280
+ tool_name: "Edit",
12281
+ tool_input: { file_path: "src/runtime.ts" },
10329
12282
  }, { cwd });
10330
- assert.equal(repeated.omxEventName, "stop");
10331
- assert.equal(repeated.outputJson?.decision, "block");
10332
- assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
10333
- assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
10334
- assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
12283
+ assert.equal(result.omxEventName, "pre-tool-use");
12284
+ assert.equal(result.outputJson, null);
10335
12285
  }
10336
12286
  finally {
10337
12287
  await rm(cwd, { recursive: true, force: true });
@@ -10866,6 +12816,91 @@ describe("codex native hook triage integration", () => {
10866
12816
  await rm(cwd, { recursive: true, force: true });
10867
12817
  }
10868
12818
  });
12819
+ it("omits Team handoff guidance from autopilot prompt context when Team mode is disabled", async () => {
12820
+ const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-observable-no-team-"));
12821
+ try {
12822
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
12823
+ await writeJson(join(cwd, ".omx", "setup-scope.json"), {
12824
+ scope: "project",
12825
+ teamMode: "disabled",
12826
+ });
12827
+ await writeSessionStart(cwd, "sess-autopilot-observable-no-team");
12828
+ const result = await dispatchCodexNativeHook({
12829
+ hook_event_name: "UserPromptSubmit",
12830
+ cwd,
12831
+ session_id: "sess-autopilot-observable-no-team",
12832
+ thread_id: "thread-autopilot-observable-no-team",
12833
+ turn_id: "turn-autopilot-observable-no-team",
12834
+ prompt: "$autopilot implement issue #2430",
12835
+ }, { cwd });
12836
+ assert.equal(result.skillState?.skill, "autopilot");
12837
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
12838
+ assert.match(additionalContext, /detected workflow keyword "\$autopilot" -> autopilot/);
12839
+ assert.match(additionalContext, /\$deep-interview -> \$ralplan -> \$ultragoal -> \$code-review -> \$ultraqa/);
12840
+ assert.doesNotMatch(additionalContext, /\$team/);
12841
+ assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
12842
+ }
12843
+ finally {
12844
+ await rm(cwd, { recursive: true, force: true });
12845
+ }
12846
+ });
12847
+ it("ignores disabled $team before outside-tmux Team blocking so later workflows can activate", async () => {
12848
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-disabled-team-primary-"));
12849
+ try {
12850
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
12851
+ await writeJson(join(cwd, ".omx", "setup-scope.json"), {
12852
+ scope: "project",
12853
+ teamMode: "disabled",
12854
+ });
12855
+ await writeSessionStart(cwd, "sess-disabled-team-primary");
12856
+ const result = await dispatchCodexNativeHook({
12857
+ hook_event_name: "UserPromptSubmit",
12858
+ cwd,
12859
+ session_id: "sess-disabled-team-primary",
12860
+ thread_id: "thread-disabled-team-primary",
12861
+ turn_id: "turn-disabled-team-primary",
12862
+ prompt: "$team $ralph fix this",
12863
+ }, { cwd });
12864
+ assert.equal(result.skillState?.skill, "ralph");
12865
+ assert.equal(result.skillState?.transition_error, undefined);
12866
+ assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
12867
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-disabled-team-primary", "ralph-state.json")), true);
12868
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
12869
+ assert.match(additionalContext, /detected workflow keyword "\$ralph" -> ralph/);
12870
+ assert.doesNotMatch(additionalContext, /Codex App\/native outside-tmux sessions cannot activate/);
12871
+ }
12872
+ finally {
12873
+ await rm(cwd, { recursive: true, force: true });
12874
+ }
12875
+ });
12876
+ it("makes bare autopilot command activation observable in state and prompt guidance", async () => {
12877
+ const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-bare-observable-"));
12878
+ try {
12879
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
12880
+ await writeSessionStart(cwd, "sess-autopilot-bare-observable");
12881
+ const result = await dispatchCodexNativeHook({
12882
+ hook_event_name: "UserPromptSubmit",
12883
+ cwd,
12884
+ session_id: "sess-autopilot-bare-observable",
12885
+ thread_id: "thread-autopilot-bare-observable",
12886
+ turn_id: "turn-autopilot-bare-observable",
12887
+ prompt: "run autopilot",
12888
+ }, { cwd });
12889
+ assert.equal(result.skillState?.skill, "autopilot");
12890
+ assert.equal(result.skillState?.phase, "deep-interview");
12891
+ assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-bare-observable/autopilot-state.json");
12892
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
12893
+ assert.match(additionalContext, /detected workflow keyword "autopilot" -> autopilot/);
12894
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
12895
+ const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-bare-observable", "autopilot-state.json");
12896
+ const modeState = JSON.parse(await readFile(statePath, "utf-8"));
12897
+ assert.equal(modeState.active, true);
12898
+ assert.equal(modeState.current_phase, "deep-interview");
12899
+ }
12900
+ finally {
12901
+ await rm(cwd, { recursive: true, force: true });
12902
+ }
12903
+ });
10869
12904
  // ── Group 2: HEAVY injection ─────────────────────────────────────────────
10870
12905
  it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
10871
12906
  const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));