oh-my-codex 0.13.2 → 0.14.1

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 (406) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +14 -8
  4. package/crates/omx-explore/src/main.rs +94 -1
  5. package/crates/omx-sparkshell/src/codex_bridge.rs +59 -12
  6. package/crates/omx-sparkshell/tests/execution.rs +48 -0
  7. package/dist/autoresearch/__tests__/skill-validation.test.d.ts +2 -0
  8. package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +1 -0
  9. package/dist/autoresearch/__tests__/skill-validation.test.js +91 -0
  10. package/dist/autoresearch/__tests__/skill-validation.test.js.map +1 -0
  11. package/dist/autoresearch/skill-validation.d.ts +13 -0
  12. package/dist/autoresearch/skill-validation.d.ts.map +1 -0
  13. package/dist/autoresearch/skill-validation.js +165 -0
  14. package/dist/autoresearch/skill-validation.js.map +1 -0
  15. package/dist/catalog/__tests__/schema.test.js +6 -0
  16. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  17. package/dist/cli/__tests__/autoresearch-guided.test.js +236 -273
  18. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  19. package/dist/cli/__tests__/autoresearch.test.js +64 -653
  20. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  21. package/dist/cli/__tests__/explore.test.js +33 -1
  22. package/dist/cli/__tests__/explore.test.js.map +1 -1
  23. package/dist/cli/__tests__/index.test.js +18 -2
  24. package/dist/cli/__tests__/index.test.js.map +1 -1
  25. package/dist/cli/__tests__/nested-help-routing.test.js +2 -1
  26. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  27. package/dist/cli/__tests__/package-bin-contract.test.js +5 -0
  28. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  29. package/dist/cli/__tests__/question.test.d.ts +2 -0
  30. package/dist/cli/__tests__/question.test.d.ts.map +1 -0
  31. package/dist/cli/__tests__/question.test.js +166 -0
  32. package/dist/cli/__tests__/question.test.js.map +1 -0
  33. package/dist/cli/__tests__/session-search-help.test.js +1 -1
  34. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  35. package/dist/cli/__tests__/setup-agents-overwrite.test.js +32 -7
  36. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-refresh.test.js +8 -6
  38. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  39. package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -0
  40. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  41. package/dist/cli/__tests__/sparkshell-cli.test.js +23 -0
  42. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  43. package/dist/cli/__tests__/uninstall.test.js +65 -5
  44. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  45. package/dist/cli/__tests__/update.test.js +360 -26
  46. package/dist/cli/__tests__/update.test.js.map +1 -1
  47. package/dist/cli/autoresearch-guided.d.ts +24 -7
  48. package/dist/cli/autoresearch-guided.d.ts.map +1 -1
  49. package/dist/cli/autoresearch-guided.js +189 -130
  50. package/dist/cli/autoresearch-guided.js.map +1 -1
  51. package/dist/cli/autoresearch.d.ts +3 -2
  52. package/dist/cli/autoresearch.d.ts.map +1 -1
  53. package/dist/cli/autoresearch.js +29 -305
  54. package/dist/cli/autoresearch.js.map +1 -1
  55. package/dist/cli/doctor.d.ts.map +1 -1
  56. package/dist/cli/doctor.js +43 -0
  57. package/dist/cli/doctor.js.map +1 -1
  58. package/dist/cli/explore.d.ts.map +1 -1
  59. package/dist/cli/explore.js +18 -3
  60. package/dist/cli/explore.js.map +1 -1
  61. package/dist/cli/index.d.ts +2 -1
  62. package/dist/cli/index.d.ts.map +1 -1
  63. package/dist/cli/index.js +15 -3
  64. package/dist/cli/index.js.map +1 -1
  65. package/dist/cli/question.d.ts +3 -0
  66. package/dist/cli/question.d.ts.map +1 -0
  67. package/dist/cli/question.js +182 -0
  68. package/dist/cli/question.js.map +1 -0
  69. package/dist/cli/setup.d.ts.map +1 -1
  70. package/dist/cli/setup.js +25 -3
  71. package/dist/cli/setup.js.map +1 -1
  72. package/dist/cli/sparkshell.d.ts.map +1 -1
  73. package/dist/cli/sparkshell.js +11 -1
  74. package/dist/cli/sparkshell.js.map +1 -1
  75. package/dist/cli/team.d.ts.map +1 -1
  76. package/dist/cli/team.js +159 -394
  77. package/dist/cli/team.js.map +1 -1
  78. package/dist/cli/uninstall.d.ts.map +1 -1
  79. package/dist/cli/uninstall.js +3 -1
  80. package/dist/cli/uninstall.js.map +1 -1
  81. package/dist/cli/update.d.ts +37 -9
  82. package/dist/cli/update.d.ts.map +1 -1
  83. package/dist/cli/update.js +204 -26
  84. package/dist/cli/update.js.map +1 -1
  85. package/dist/config/__tests__/generator-idempotent.test.js +51 -14
  86. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  87. package/dist/config/__tests__/generator-notify.test.js +35 -10
  88. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  89. package/dist/config/generator.d.ts +1 -0
  90. package/dist/config/generator.d.ts.map +1 -1
  91. package/dist/config/generator.js +61 -7
  92. package/dist/config/generator.js.map +1 -1
  93. package/dist/hooks/__tests__/analyze-routing-contract.test.js +22 -13
  94. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  95. package/dist/hooks/__tests__/anti-slop-workflow.test.js +3 -3
  96. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  97. package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts +2 -0
  98. package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts.map +1 -0
  99. package/dist/hooks/__tests__/code-review-skill-contract.test.js +56 -0
  100. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -0
  101. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +2 -2
  102. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +1 -1
  103. package/dist/hooks/__tests__/deep-interview-contract.test.js +51 -5
  104. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  105. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts +2 -0
  106. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts.map +1 -0
  107. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js +43 -0
  108. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js.map +1 -0
  109. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts +2 -0
  110. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts.map +1 -0
  111. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js +38 -0
  112. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js.map +1 -0
  113. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +2 -2
  114. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  115. package/dist/hooks/__tests__/keyword-detector.test.js +308 -17
  116. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  117. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +570 -2
  118. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  119. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +717 -16
  120. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  121. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +25 -0
  122. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  123. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +894 -1
  124. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  125. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +34 -0
  126. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  127. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +132 -0
  128. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  129. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +22 -4
  130. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  131. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +4 -2
  132. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  133. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +1 -0
  134. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +1 -1
  135. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +19 -1
  136. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  137. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +28 -0
  138. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  139. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +5 -4
  140. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +1 -1
  141. package/dist/hooks/__tests__/prompt-team-routing.test.js +2 -2
  142. package/dist/hooks/__tests__/prompt-team-routing.test.js.map +1 -1
  143. package/dist/hooks/__tests__/triage-config.test.d.ts +2 -0
  144. package/dist/hooks/__tests__/triage-config.test.d.ts.map +1 -0
  145. package/dist/hooks/__tests__/triage-config.test.js +211 -0
  146. package/dist/hooks/__tests__/triage-config.test.js.map +1 -0
  147. package/dist/hooks/__tests__/triage-heuristic.test.d.ts +2 -0
  148. package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +1 -0
  149. package/dist/hooks/__tests__/triage-heuristic.test.js +230 -0
  150. package/dist/hooks/__tests__/triage-heuristic.test.js.map +1 -0
  151. package/dist/hooks/__tests__/triage-state.test.d.ts +2 -0
  152. package/dist/hooks/__tests__/triage-state.test.d.ts.map +1 -0
  153. package/dist/hooks/__tests__/triage-state.test.js +426 -0
  154. package/dist/hooks/__tests__/triage-state.test.js.map +1 -0
  155. package/dist/hooks/keyword-detector.d.ts +26 -7
  156. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  157. package/dist/hooks/keyword-detector.js +97 -26
  158. package/dist/hooks/keyword-detector.js.map +1 -1
  159. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  160. package/dist/hooks/keyword-registry.js +16 -9
  161. package/dist/hooks/keyword-registry.js.map +1 -1
  162. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  163. package/dist/hooks/prompt-guidance-contract.js +28 -1
  164. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  165. package/dist/hooks/triage-config.d.ts +33 -0
  166. package/dist/hooks/triage-config.d.ts.map +1 -0
  167. package/dist/hooks/triage-config.js +87 -0
  168. package/dist/hooks/triage-config.js.map +1 -0
  169. package/dist/hooks/triage-heuristic.d.ts +20 -0
  170. package/dist/hooks/triage-heuristic.d.ts.map +1 -0
  171. package/dist/hooks/triage-heuristic.js +210 -0
  172. package/dist/hooks/triage-heuristic.js.map +1 -0
  173. package/dist/hooks/triage-state.d.ts +63 -0
  174. package/dist/hooks/triage-state.d.ts.map +1 -0
  175. package/dist/hooks/triage-state.js +138 -0
  176. package/dist/hooks/triage-state.js.map +1 -0
  177. package/dist/hud/__tests__/reconcile.test.js +20 -0
  178. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  179. package/dist/hud/reconcile.d.ts +1 -0
  180. package/dist/hud/reconcile.d.ts.map +1 -1
  181. package/dist/hud/reconcile.js +2 -1
  182. package/dist/hud/reconcile.js.map +1 -1
  183. package/dist/mcp/__tests__/bootstrap.test.js +5 -24
  184. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  185. package/dist/mcp/__tests__/state-server.test.js +127 -0
  186. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  187. package/dist/mcp/bootstrap.d.ts +1 -1
  188. package/dist/mcp/bootstrap.d.ts.map +1 -1
  189. package/dist/mcp/bootstrap.js +3 -11
  190. package/dist/mcp/bootstrap.js.map +1 -1
  191. package/dist/mcp/state-server.d.ts +25 -0
  192. package/dist/mcp/state-server.d.ts.map +1 -1
  193. package/dist/mcp/state-server.js +41 -0
  194. package/dist/mcp/state-server.js.map +1 -1
  195. package/dist/modes/__tests__/base-ralph-contract.test.js +15 -0
  196. package/dist/modes/__tests__/base-ralph-contract.test.js.map +1 -1
  197. package/dist/modes/base.d.ts +1 -0
  198. package/dist/modes/base.d.ts.map +1 -1
  199. package/dist/modes/base.js +22 -6
  200. package/dist/modes/base.js.map +1 -1
  201. package/dist/notifications/__tests__/index.test.js +75 -0
  202. package/dist/notifications/__tests__/index.test.js.map +1 -1
  203. package/dist/notifications/__tests__/session-status.test.js +90 -0
  204. package/dist/notifications/__tests__/session-status.test.js.map +1 -1
  205. package/dist/notifications/index.d.ts.map +1 -1
  206. package/dist/notifications/index.js +39 -22
  207. package/dist/notifications/index.js.map +1 -1
  208. package/dist/notifications/session-status.d.ts +2 -0
  209. package/dist/notifications/session-status.d.ts.map +1 -1
  210. package/dist/notifications/session-status.js +19 -4
  211. package/dist/notifications/session-status.js.map +1 -1
  212. package/dist/openclaw/index.d.ts +5 -3
  213. package/dist/openclaw/index.d.ts.map +1 -1
  214. package/dist/openclaw/index.js +5 -3
  215. package/dist/openclaw/index.js.map +1 -1
  216. package/dist/question/__tests__/client.test.d.ts +2 -0
  217. package/dist/question/__tests__/client.test.d.ts.map +1 -0
  218. package/dist/question/__tests__/client.test.js +70 -0
  219. package/dist/question/__tests__/client.test.js.map +1 -0
  220. package/dist/question/__tests__/deep-interview.test.d.ts +2 -0
  221. package/dist/question/__tests__/deep-interview.test.d.ts.map +1 -0
  222. package/dist/question/__tests__/deep-interview.test.js +118 -0
  223. package/dist/question/__tests__/deep-interview.test.js.map +1 -0
  224. package/dist/question/__tests__/policy.test.d.ts +2 -0
  225. package/dist/question/__tests__/policy.test.d.ts.map +1 -0
  226. package/dist/question/__tests__/policy.test.js +107 -0
  227. package/dist/question/__tests__/policy.test.js.map +1 -0
  228. package/dist/question/__tests__/renderer.test.d.ts +2 -0
  229. package/dist/question/__tests__/renderer.test.d.ts.map +1 -0
  230. package/dist/question/__tests__/renderer.test.js +238 -0
  231. package/dist/question/__tests__/renderer.test.js.map +1 -0
  232. package/dist/question/__tests__/state.test.d.ts +2 -0
  233. package/dist/question/__tests__/state.test.d.ts.map +1 -0
  234. package/dist/question/__tests__/state.test.js +75 -0
  235. package/dist/question/__tests__/state.test.js.map +1 -0
  236. package/dist/question/__tests__/types.test.d.ts +2 -0
  237. package/dist/question/__tests__/types.test.d.ts.map +1 -0
  238. package/dist/question/__tests__/types.test.js +44 -0
  239. package/dist/question/__tests__/types.test.js.map +1 -0
  240. package/dist/question/__tests__/ui.test.d.ts +2 -0
  241. package/dist/question/__tests__/ui.test.d.ts.map +1 -0
  242. package/dist/question/__tests__/ui.test.js +169 -0
  243. package/dist/question/__tests__/ui.test.js.map +1 -0
  244. package/dist/question/client.d.ts +54 -0
  245. package/dist/question/client.d.ts.map +1 -0
  246. package/dist/question/client.js +77 -0
  247. package/dist/question/client.js.map +1 -0
  248. package/dist/question/deep-interview.d.ts +30 -0
  249. package/dist/question/deep-interview.d.ts.map +1 -0
  250. package/dist/question/deep-interview.js +118 -0
  251. package/dist/question/deep-interview.js.map +1 -0
  252. package/dist/question/policy.d.ts +18 -0
  253. package/dist/question/policy.d.ts.map +1 -0
  254. package/dist/question/policy.js +77 -0
  255. package/dist/question/policy.js.map +1 -0
  256. package/dist/question/renderer.d.ts +20 -0
  257. package/dist/question/renderer.d.ts.map +1 -0
  258. package/dist/question/renderer.js +190 -0
  259. package/dist/question/renderer.js.map +1 -0
  260. package/dist/question/state.d.ts +19 -0
  261. package/dist/question/state.d.ts.map +1 -0
  262. package/dist/question/state.js +108 -0
  263. package/dist/question/state.js.map +1 -0
  264. package/dist/question/types.d.ts +66 -0
  265. package/dist/question/types.d.ts.map +1 -0
  266. package/dist/question/types.js +82 -0
  267. package/dist/question/types.js.map +1 -0
  268. package/dist/question/ui.d.ts +38 -0
  269. package/dist/question/ui.d.ts.map +1 -0
  270. package/dist/question/ui.js +321 -0
  271. package/dist/question/ui.js.map +1 -0
  272. package/dist/ralph/contract.d.ts +1 -1
  273. package/dist/ralph/contract.d.ts.map +1 -1
  274. package/dist/ralph/contract.js +4 -1
  275. package/dist/ralph/contract.js.map +1 -1
  276. package/dist/ralplan/runtime.js +1 -1
  277. package/dist/ralplan/runtime.js.map +1 -1
  278. package/dist/runtime/__tests__/run-loop.test.d.ts +2 -0
  279. package/dist/runtime/__tests__/run-loop.test.d.ts.map +1 -0
  280. package/dist/runtime/__tests__/run-loop.test.js +35 -0
  281. package/dist/runtime/__tests__/run-loop.test.js.map +1 -0
  282. package/dist/runtime/__tests__/run-outcome.test.d.ts +2 -0
  283. package/dist/runtime/__tests__/run-outcome.test.d.ts.map +1 -0
  284. package/dist/runtime/__tests__/run-outcome.test.js +102 -0
  285. package/dist/runtime/__tests__/run-outcome.test.js.map +1 -0
  286. package/dist/runtime/__tests__/run-state.test.d.ts +2 -0
  287. package/dist/runtime/__tests__/run-state.test.d.ts.map +1 -0
  288. package/dist/runtime/__tests__/run-state.test.js +37 -0
  289. package/dist/runtime/__tests__/run-state.test.js.map +1 -0
  290. package/dist/runtime/run-loop.d.ts +45 -0
  291. package/dist/runtime/run-loop.d.ts.map +1 -0
  292. package/dist/runtime/run-loop.js +51 -0
  293. package/dist/runtime/run-loop.js.map +1 -0
  294. package/dist/runtime/run-outcome.d.ts +46 -0
  295. package/dist/runtime/run-outcome.d.ts.map +1 -0
  296. package/dist/runtime/run-outcome.js +285 -0
  297. package/dist/runtime/run-outcome.js.map +1 -0
  298. package/dist/runtime/run-state.d.ts +40 -0
  299. package/dist/runtime/run-state.d.ts.map +1 -0
  300. package/dist/runtime/run-state.js +120 -0
  301. package/dist/runtime/run-state.js.map +1 -0
  302. package/dist/runtime/terminal-lifecycle.d.ts +11 -0
  303. package/dist/runtime/terminal-lifecycle.d.ts.map +1 -0
  304. package/dist/runtime/terminal-lifecycle.js +52 -0
  305. package/dist/runtime/terminal-lifecycle.js.map +1 -0
  306. package/dist/scripts/__tests__/codex-native-hook.test.js +1459 -126
  307. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  308. package/dist/scripts/__tests__/postinstall.test.d.ts +2 -0
  309. package/dist/scripts/__tests__/postinstall.test.d.ts.map +1 -0
  310. package/dist/scripts/__tests__/postinstall.test.js +178 -0
  311. package/dist/scripts/__tests__/postinstall.test.js.map +1 -0
  312. package/dist/scripts/codex-native-hook.d.ts +3 -0
  313. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  314. package/dist/scripts/codex-native-hook.js +308 -61
  315. package/dist/scripts/codex-native-hook.js.map +1 -1
  316. package/dist/scripts/notify-fallback-watcher.js +81 -2
  317. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  318. package/dist/scripts/notify-hook/auto-nudge.d.ts +27 -0
  319. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  320. package/dist/scripts/notify-hook/auto-nudge.js +83 -20
  321. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  322. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  323. package/dist/scripts/notify-hook/managed-tmux.js +64 -38
  324. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  325. package/dist/scripts/notify-hook/ralph-session-resume.js +1 -1
  326. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  327. package/dist/scripts/notify-hook.js +15 -5
  328. package/dist/scripts/notify-hook.js.map +1 -1
  329. package/dist/scripts/postinstall.d.ts +22 -0
  330. package/dist/scripts/postinstall.d.ts.map +1 -0
  331. package/dist/scripts/postinstall.js +105 -0
  332. package/dist/scripts/postinstall.js.map +1 -0
  333. package/dist/scripts/sync-prompt-guidance-fragments.js +5 -0
  334. package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
  335. package/dist/state/__tests__/operations-ralph-phase.test.js +21 -0
  336. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  337. package/dist/state/__tests__/operations.test.js +18 -0
  338. package/dist/state/__tests__/operations.test.js.map +1 -1
  339. package/dist/state/__tests__/workflow-transition.test.js +11 -0
  340. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  341. package/dist/state/operations.d.ts.map +1 -1
  342. package/dist/state/operations.js +15 -0
  343. package/dist/state/operations.js.map +1 -1
  344. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  345. package/dist/state/workflow-transition-reconcile.js +14 -1
  346. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  347. package/dist/state/workflow-transition.d.ts.map +1 -1
  348. package/dist/state/workflow-transition.js +3 -1
  349. package/dist/state/workflow-transition.js.map +1 -1
  350. package/dist/team/__tests__/followup-planner.test.js +15 -0
  351. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  352. package/dist/team/__tests__/role-router.test.js +47 -0
  353. package/dist/team/__tests__/role-router.test.js.map +1 -1
  354. package/dist/team/__tests__/runtime.test.js +108 -2
  355. package/dist/team/__tests__/runtime.test.js.map +1 -1
  356. package/dist/team/followup-planner.d.ts.map +1 -1
  357. package/dist/team/followup-planner.js +31 -9
  358. package/dist/team/followup-planner.js.map +1 -1
  359. package/dist/team/role-router.d.ts.map +1 -1
  360. package/dist/team/role-router.js +73 -0
  361. package/dist/team/role-router.js.map +1 -1
  362. package/dist/team/runtime.d.ts.map +1 -1
  363. package/dist/team/runtime.js +18 -4
  364. package/dist/team/runtime.js.map +1 -1
  365. package/dist/utils/__tests__/dep-versions.test.js +25 -8
  366. package/dist/utils/__tests__/dep-versions.test.js.map +1 -1
  367. package/dist/utils/__tests__/paths.test.js +45 -0
  368. package/dist/utils/__tests__/paths.test.js.map +1 -1
  369. package/dist/utils/paths.d.ts +2 -0
  370. package/dist/utils/paths.d.ts.map +1 -1
  371. package/dist/utils/paths.js +22 -7
  372. package/dist/utils/paths.js.map +1 -1
  373. package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
  374. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  375. package/package.json +4 -2
  376. package/prompts/architect.md +4 -0
  377. package/prompts/code-reviewer.md +3 -0
  378. package/prompts/dependency-expert.md +3 -0
  379. package/prompts/executor.md +5 -0
  380. package/prompts/explore.md +2 -0
  381. package/prompts/planner.md +5 -0
  382. package/prompts/product-analyst.md +8 -8
  383. package/prompts/researcher.md +78 -30
  384. package/prompts/verifier.md +4 -0
  385. package/skills/autoresearch/SKILL.md +68 -0
  386. package/skills/code-review/SKILL.md +94 -28
  387. package/skills/deep-interview/SKILL.md +100 -9
  388. package/skills/help/SKILL.md +3 -1
  389. package/skills/ralplan/SKILL.md +1 -0
  390. package/skills/team/SKILL.md +1 -0
  391. package/skills/ultrawork/SKILL.md +1 -0
  392. package/src/scripts/__tests__/codex-native-hook.test.ts +2373 -692
  393. package/src/scripts/__tests__/postinstall.test.ts +210 -0
  394. package/src/scripts/codex-native-hook.ts +365 -66
  395. package/src/scripts/notify-fallback-watcher.ts +92 -2
  396. package/src/scripts/notify-hook/auto-nudge.ts +89 -20
  397. package/src/scripts/notify-hook/managed-tmux.ts +70 -31
  398. package/src/scripts/notify-hook/ralph-session-resume.ts +1 -1
  399. package/src/scripts/notify-hook.ts +23 -5
  400. package/src/scripts/postinstall-bootstrap.js +23 -0
  401. package/src/scripts/postinstall.ts +161 -0
  402. package/src/scripts/sync-prompt-guidance-fragments.ts +4 -0
  403. package/templates/AGENTS.md +48 -37
  404. package/templates/catalog-manifest.json +7 -0
  405. package/templates/model-instructions/explore-lightweight-AGENTS.md +11 -0
  406. package/templates/model-instructions/sparkshell-lightweight-AGENTS.md +10 -0
@@ -4,15 +4,36 @@ import { existsSync } from "node:fs";
4
4
  import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
7
8
  import { afterEach, beforeEach, describe, it } from "node:test";
8
9
  import { buildManagedCodexHooksConfig } from "../../config/codex-hooks.js";
9
10
  import { initTeamState, readTeamLeaderAttention, readTeamPhase, writeTeamLeaderAttention, } from "../../team/state.js";
10
- import { dispatchCodexNativeHook, mapCodexHookEventToOmxEvent, resolveSessionOwnerPidFromAncestry, } from "../codex-native-hook.js";
11
+ import { dispatchCodexNativeHook, isCodexNativeHookMainModule, mapCodexHookEventToOmxEvent, resolveSessionOwnerPidFromAncestry, } from "../codex-native-hook.js";
11
12
  import { writeSessionStart } from "../../hooks/session.js";
13
+ import { resetTriageConfigCache } from "../../hooks/triage-config.js";
12
14
  async function writeJson(path, value) {
13
15
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
14
16
  await writeFile(path, JSON.stringify(value, null, 2));
15
17
  }
18
+ async function writeHookCounterPlugin(cwd) {
19
+ const markerPath = join(cwd, ".omx", "stop-hook-counter.json");
20
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
21
+ await writeFile(join(cwd, ".omx", "hooks", "count-stop-hook.mjs"), `import { mkdir, readFile, writeFile } from "node:fs/promises";
22
+ import { dirname, join } from "node:path";
23
+
24
+ export async function onHookEvent(event) {
25
+ if (event.event !== "stop") return;
26
+ const outPath = join(process.cwd(), ".omx", "stop-hook-counter.json");
27
+ await mkdir(dirname(outPath), { recursive: true });
28
+ let count = 0;
29
+ try {
30
+ count = JSON.parse(await readFile(outPath, "utf-8")).count || 0;
31
+ } catch {}
32
+ await writeFile(outPath, JSON.stringify({ count: count + 1 }, null, 2));
33
+ }
34
+ `, "utf-8");
35
+ return markerPath;
36
+ }
16
37
  async function writeReleaseReadinessLeaderAttention(teamName, sessionId, cwd, options) {
17
38
  await writeTeamLeaderAttention(teamName, {
18
39
  team_name: teamName,
@@ -90,6 +111,13 @@ describe("codex native hook config", () => {
90
111
  });
91
112
  });
92
113
  describe("codex native hook dispatch", () => {
114
+ it("treats space-containing argv entry paths as the main module", () => {
115
+ const entryPath = "/tmp/omx native/codex-native-hook.js";
116
+ assert.equal(isCodexNativeHookMainModule(pathToFileURL(entryPath).href, entryPath), true);
117
+ });
118
+ it("does not treat a different module url as the main module", () => {
119
+ assert.equal(isCodexNativeHookMainModule(pathToFileURL("/tmp/omx native/other-script.js").href, "/tmp/omx native/codex-native-hook.js"), false);
120
+ });
93
121
  it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
94
122
  const stdout = execFileSync(process.execPath, [join(process.cwd(), "dist", "scripts", "codex-native-hook.js")], {
95
123
  cwd: process.cwd(),
@@ -174,7 +202,41 @@ describe("codex native hook dispatch", () => {
174
202
  await rm(cwd, { recursive: true, force: true });
175
203
  }
176
204
  });
177
- it("appends .omx/ to repo-root .gitignore during SessionStart when missing", async () => {
205
+ it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
206
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
207
+ try {
208
+ const stateDir = join(cwd, ".omx", "state");
209
+ const canonicalSessionId = "omx-launch-hud";
210
+ const nativeSessionId = "codex-native-hud";
211
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
212
+ await writeSessionStart(cwd, canonicalSessionId);
213
+ let reconcileCall = null;
214
+ const promptResult = await dispatchCodexNativeHook({
215
+ hook_event_name: "UserPromptSubmit",
216
+ cwd,
217
+ session_id: nativeSessionId,
218
+ thread_id: "thread-hud",
219
+ turn_id: "turn-hud",
220
+ prompt: "$ralplan fix orphaned hud session handoff",
221
+ }, {
222
+ cwd,
223
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
224
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
225
+ return { status: 'recreated', paneId: '%9', desiredHeight: 3, duplicateCount: 0 };
226
+ },
227
+ });
228
+ assert.equal(promptResult.omxEventName, "keyword-detector");
229
+ assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
230
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), true);
231
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), true);
232
+ assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json")), false);
233
+ assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "ralplan-state.json")), false);
234
+ }
235
+ finally {
236
+ await rm(cwd, { recursive: true, force: true });
237
+ }
238
+ });
239
+ it("adds .omx/ to git info/exclude during SessionStart instead of mutating repo .gitignore", async () => {
178
240
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-gitignore-"));
179
241
  try {
180
242
  await writeFile(join(cwd, ".gitignore"), "node_modules/\n");
@@ -186,8 +248,55 @@ describe("codex native hook dispatch", () => {
186
248
  }, { cwd, sessionOwnerPid: 43210 });
187
249
  assert.equal(result.omxEventName, "session-start");
188
250
  const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
189
- assert.match(gitignore, /^node_modules\/\n\.omx\/\n$/);
190
- assert.match(JSON.stringify(result.outputJson), /Added \.omx\/ to .*\.gitignore/);
251
+ assert.equal(gitignore, "node_modules/\n");
252
+ const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
253
+ assert.match(exclude, /(?:^|\n)\.omx\/\n/);
254
+ assert.match(JSON.stringify(result.outputJson), /Added \.omx\/ to .*\.git[\/]info[\/]exclude/);
255
+ }
256
+ finally {
257
+ await rm(cwd, { recursive: true, force: true });
258
+ }
259
+ });
260
+ it("keeps SessionStart quiet when .omx/ is already ignored by repo-level gitignore", async () => {
261
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-existing-ignore-"));
262
+ try {
263
+ await writeFile(join(cwd, ".gitignore"), "node_modules/\n.omx/\n");
264
+ execFileSync("git", ["init"], { cwd, stdio: "pipe" });
265
+ const result = await dispatchCodexNativeHook({
266
+ hook_event_name: "SessionStart",
267
+ cwd,
268
+ session_id: "sess-gitignore-existing",
269
+ }, { cwd, sessionOwnerPid: 43210 });
270
+ assert.equal(result.omxEventName, "session-start");
271
+ const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
272
+ assert.equal(gitignore, "node_modules/\n.omx/\n");
273
+ const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
274
+ assert.doesNotMatch(exclude, /(?:^|\n)\.omx\/\n/);
275
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /Added \.omx\//);
276
+ }
277
+ finally {
278
+ await rm(cwd, { recursive: true, force: true });
279
+ }
280
+ });
281
+ it("respects existing Git ignore resolution before writing local excludes", async () => {
282
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-global-ignore-"));
283
+ const excludesFile = join(cwd, "global-ignore");
284
+ try {
285
+ await writeFile(join(cwd, ".gitignore"), "node_modules/\n");
286
+ await writeFile(excludesFile, ".omx/\n");
287
+ execFileSync("git", ["init"], { cwd, stdio: "pipe" });
288
+ execFileSync("git", ["config", "core.excludesfile", excludesFile], { cwd, stdio: "pipe" });
289
+ const result = await dispatchCodexNativeHook({
290
+ hook_event_name: "SessionStart",
291
+ cwd,
292
+ session_id: "sess-gitignore-global",
293
+ }, { cwd, sessionOwnerPid: 43210 });
294
+ assert.equal(result.omxEventName, "session-start");
295
+ const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
296
+ assert.equal(gitignore, "node_modules/\n");
297
+ const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
298
+ assert.doesNotMatch(exclude, /(?:^|\n)\.omx\/\n/);
299
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /Added \.omx\//);
191
300
  }
192
301
  finally {
193
302
  await rm(cwd, { recursive: true, force: true });
@@ -352,7 +461,12 @@ describe("codex native hook dispatch", () => {
352
461
  }, { cwd });
353
462
  assert.equal(result.omxEventName, "keyword-detector");
354
463
  assert.equal(result.skillState, null);
355
- assert.equal(result.outputJson, null);
464
+ // Triage may inject advisory LIGHT/explore context for the question-shaped
465
+ // prompt, but the invariant this test guards is that no Ralph workflow state
466
+ // is seeded and no Ralph-activation message is emitted.
467
+ const advisoryContext = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
468
+ assert.doesNotMatch(advisoryContext, /skill:\s*ralph/i);
469
+ assert.doesNotMatch(advisoryContext, /ralph-state\.json/i);
356
470
  assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
357
471
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "skill-active-state.json")), false);
358
472
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "ralph-state.json")), false);
@@ -361,6 +475,54 @@ describe("codex native hook dispatch", () => {
361
475
  await rm(cwd, { recursive: true, force: true });
362
476
  }
363
477
  });
478
+ it("adds execution handoff context for non-keyword prompts that authorize implementation", async () => {
479
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-execution-handoff-"));
480
+ try {
481
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
482
+ const prompts = [
483
+ "按照这个plan开始执行优化",
484
+ "开始执行",
485
+ "继续优化",
486
+ "直接修复",
487
+ ];
488
+ for (const [index, prompt] of prompts.entries()) {
489
+ const result = await dispatchCodexNativeHook({
490
+ hook_event_name: "UserPromptSubmit",
491
+ cwd,
492
+ session_id: `sess-exec-handoff-${index}`,
493
+ thread_id: `thread-exec-handoff-${index}`,
494
+ turn_id: `turn-exec-handoff-${index}`,
495
+ prompt,
496
+ }, { cwd });
497
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
498
+ assert.match(message, /execution handoff/i, prompt);
499
+ assert.match(message, /Do not restate the prior plan/i, prompt);
500
+ }
501
+ }
502
+ finally {
503
+ await rm(cwd, { recursive: true, force: true });
504
+ }
505
+ });
506
+ it("adds latest-followup priority context for short same-thread follow-up prompts", async () => {
507
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-followup-priority-"));
508
+ try {
509
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
510
+ const result = await dispatchCodexNativeHook({
511
+ hook_event_name: "UserPromptSubmit",
512
+ cwd,
513
+ session_id: "sess-followup-priority",
514
+ thread_id: "thread-followup-priority",
515
+ turn_id: "turn-followup-priority",
516
+ prompt: "这些优化都做了么",
517
+ }, { cwd });
518
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
519
+ assert.match(message, /same-thread follow-up/i);
520
+ assert.match(message, /prefer it over older unresolved prompts/i);
521
+ }
522
+ finally {
523
+ await rm(cwd, { recursive: true, force: true });
524
+ }
525
+ });
364
526
  it("clarifies that prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
365
527
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-routing-"));
366
528
  try {
@@ -385,6 +547,125 @@ describe("codex native hook dispatch", () => {
385
547
  await rm(cwd, { recursive: true, force: true });
386
548
  }
387
549
  });
550
+ it("keeps bare keep-going continuation on the active autopilot skill instead of denying with generic ralph overlap", async () => {
551
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-bare-continuation-"));
552
+ try {
553
+ const sessionId = "sess-autopilot-cont";
554
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
555
+ await mkdir(sessionDir, { recursive: true });
556
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
557
+ version: 1,
558
+ active: true,
559
+ skill: "autopilot",
560
+ keyword: "$autopilot",
561
+ phase: "planning",
562
+ session_id: sessionId,
563
+ active_skills: [
564
+ { skill: "autopilot", phase: "planning", active: true, session_id: sessionId },
565
+ ],
566
+ });
567
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
568
+ active: true,
569
+ mode: "autopilot",
570
+ current_phase: "execution",
571
+ started_at: "2026-04-19T00:00:00.000Z",
572
+ updated_at: "2026-04-19T00:10:00.000Z",
573
+ session_id: sessionId,
574
+ });
575
+ const result = await dispatchCodexNativeHook({
576
+ hook_event_name: "UserPromptSubmit",
577
+ cwd,
578
+ session_id: sessionId,
579
+ thread_id: "thread-autopilot-cont",
580
+ turn_id: "turn-autopilot-cont",
581
+ prompt: "\ keep going now",
582
+ }, { cwd });
583
+ assert.equal(result.omxEventName, "keyword-detector");
584
+ assert.equal(result.skillState?.skill, "autopilot");
585
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
586
+ assert.match(message, /"keep going" -> ralph/);
587
+ assert.doesNotMatch(message, /denied workflow keyword/i);
588
+ assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
589
+ assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
590
+ assert.equal(existsSync(join(sessionDir, "ralph-state.json")), false);
591
+ }
592
+ finally {
593
+ await rm(cwd, { recursive: true, force: true });
594
+ }
595
+ });
596
+ it("clarifies that prompt-side deep-interview activation must use omx question", async () => {
597
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
598
+ try {
599
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
600
+ const result = await dispatchCodexNativeHook({
601
+ hook_event_name: "UserPromptSubmit",
602
+ cwd,
603
+ session_id: "sess-deep-interview-msg",
604
+ thread_id: "thread-deep-interview-msg",
605
+ turn_id: "turn-deep-interview-msg",
606
+ prompt: "$deep-interview gather requirements",
607
+ }, { cwd });
608
+ assert.equal(result.omxEventName, "keyword-detector");
609
+ assert.equal(result.skillState?.skill, "deep-interview");
610
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
611
+ assert.match(message, /\$deep-interview" -> deep-interview/);
612
+ assert.match(message, /skill: deep-interview activated and initial state initialized at \.omx\/state\/sessions\/sess-deep-interview-msg\/deep-interview-state\.json; write subsequent updates via omx_state MCP\./);
613
+ assert.match(message, /Deep-interview must ask each interview round via `omx question`/);
614
+ assert.match(message, /do not fall back to `request_user_input` or plain-text questioning/i);
615
+ assert.match(message, /If bare `omx question` is unavailable in this reused session, use the current-session CLI bridge command:/);
616
+ assert.match(message, /`'.+' '.+dist\/cli\/omx\.js' question`/);
617
+ assert.match(message, /Stop remains blocked while a deep-interview question obligation is pending\./);
618
+ }
619
+ finally {
620
+ await rm(cwd, { recursive: true, force: true });
621
+ }
622
+ });
623
+ it("keeps bare keep-going continuation on the active ralph skill without resetting through generic keep-going routing", async () => {
624
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-bare-continuation-"));
625
+ try {
626
+ const sessionId = "sess-ralph-cont";
627
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
628
+ await mkdir(sessionDir, { recursive: true });
629
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
630
+ version: 1,
631
+ active: true,
632
+ skill: "ralph",
633
+ keyword: "$ralph",
634
+ phase: "executing",
635
+ session_id: sessionId,
636
+ active_skills: [
637
+ { skill: "ralph", phase: "executing", active: true, session_id: sessionId },
638
+ ],
639
+ });
640
+ await writeJson(join(sessionDir, "ralph-state.json"), {
641
+ active: true,
642
+ mode: "ralph",
643
+ current_phase: "verifying",
644
+ started_at: "2026-04-19T00:00:00.000Z",
645
+ updated_at: "2026-04-19T00:10:00.000Z",
646
+ iteration: 4,
647
+ max_iterations: 50,
648
+ session_id: sessionId,
649
+ });
650
+ const result = await dispatchCodexNativeHook({
651
+ hook_event_name: "UserPromptSubmit",
652
+ cwd,
653
+ session_id: sessionId,
654
+ thread_id: "thread-ralph-cont",
655
+ turn_id: "turn-ralph-cont",
656
+ prompt: "keep going now",
657
+ }, { cwd });
658
+ assert.equal(result.omxEventName, "keyword-detector");
659
+ assert.equal(result.skillState?.skill, "ralph");
660
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
661
+ assert.match(message, /"keep going" -> ralph/);
662
+ assert.doesNotMatch(message, /denied workflow keyword/i);
663
+ assert.doesNotMatch(message, /mode transiting:/);
664
+ }
665
+ finally {
666
+ await rm(cwd, { recursive: true, force: true });
667
+ }
668
+ });
388
669
  it("ignores generic wrapper fields so metadata cannot trigger workflow routing or Stop blocking", async () => {
389
670
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-wrapper-metadata-"));
390
671
  try {
@@ -1517,6 +1798,28 @@ esac
1517
1798
  await rm(cwd, { recursive: true, force: true });
1518
1799
  }
1519
1800
  });
1801
+ it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
1802
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
1803
+ try {
1804
+ const stateDir = join(cwd, ".omx", "state");
1805
+ await mkdir(stateDir, { recursive: true });
1806
+ await writeJson(join(stateDir, "autopilot-state.json"), {
1807
+ active: true,
1808
+ current_phase: "execution",
1809
+ run_outcome: "blocked_on_user",
1810
+ });
1811
+ const result = await dispatchCodexNativeHook({
1812
+ hook_event_name: "Stop",
1813
+ cwd,
1814
+ session_id: "sess-stop-autopilot-blocked-outcome",
1815
+ }, { cwd });
1816
+ assert.equal(result.omxEventName, "stop");
1817
+ assert.equal(result.outputJson, null);
1818
+ }
1819
+ finally {
1820
+ await rm(cwd, { recursive: true, force: true });
1821
+ }
1822
+ });
1520
1823
  it("returns Stop continuation output while Ultrawork is active", async () => {
1521
1824
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
1522
1825
  try {
@@ -2082,139 +2385,534 @@ esac
2082
2385
  await rm(cwd, { recursive: true, force: true });
2083
2386
  }
2084
2387
  });
2085
- it("does not block Stop solely because deep-interview is active", async () => {
2086
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
2388
+ it("blocks Stop while autoresearch is active without validator completion", async () => {
2389
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-"));
2087
2390
  try {
2088
2391
  const stateDir = join(cwd, ".omx", "state");
2089
- await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview"), { recursive: true });
2090
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview" });
2091
- await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "skill-active-state.json"), {
2092
- active: true,
2093
- skill: "deep-interview",
2094
- phase: "planning",
2095
- });
2096
- await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
2392
+ await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch"), { recursive: true });
2393
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch", cwd });
2394
+ await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch", "autoresearch-state.json"), {
2097
2395
  active: true,
2098
- current_phase: "planning",
2396
+ mode: "autoresearch",
2397
+ current_phase: "executing",
2398
+ session_id: "sess-stop-autoresearch",
2399
+ validation_mode: "mission-validator-script",
2400
+ mission_validator_command: "node scripts/validate.js",
2401
+ completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
2099
2402
  });
2100
2403
  const result = await dispatchCodexNativeHook({
2101
2404
  hook_event_name: "Stop",
2102
2405
  cwd,
2103
- session_id: "sess-stop-deep-interview",
2406
+ session_id: "sess-stop-autoresearch",
2104
2407
  }, { cwd });
2105
- assert.equal(result.outputJson, null);
2408
+ assert.equal(result.omxEventName, "stop");
2409
+ assert.deepEqual(result.outputJson, {
2410
+ decision: "block",
2411
+ reason: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
2412
+ stopReason: "autoresearch_executing",
2413
+ systemMessage: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
2414
+ });
2106
2415
  }
2107
2416
  finally {
2108
2417
  await rm(cwd, { recursive: true, force: true });
2109
2418
  }
2110
2419
  });
2111
- it("ignores root skill-active fallback from a different thread when evaluating Stop", async () => {
2112
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-thread-"));
2420
+ it("allows Stop once autoresearch validator evidence is complete", async () => {
2421
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-complete-"));
2113
2422
  try {
2114
2423
  const stateDir = join(cwd, ".omx", "state");
2115
- await mkdir(stateDir, { recursive: true });
2116
- await writeJson(join(stateDir, "skill-active-state.json"), {
2424
+ const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
2425
+ await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch-complete"), { recursive: true });
2426
+ await mkdir(specDir, { recursive: true });
2427
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch-complete", cwd });
2428
+ await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch-complete", "autoresearch-state.json"), {
2117
2429
  active: true,
2118
- skill: "deep-interview",
2119
- phase: "planning",
2120
- session_id: "",
2121
- thread_id: "other-thread",
2430
+ mode: "autoresearch",
2431
+ current_phase: "reviewing",
2432
+ session_id: "sess-stop-autoresearch-complete",
2433
+ validation_mode: "mission-validator-script",
2434
+ mission_validator_command: "node scripts/validate.js",
2435
+ completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
2122
2436
  });
2437
+ await writeJson(join(specDir, 'completion.json'), { status: 'passed', passed: true });
2123
2438
  const result = await dispatchCodexNativeHook({
2124
2439
  hook_event_name: "Stop",
2125
2440
  cwd,
2126
- session_id: "sess-stop-main",
2127
- thread_id: "main-thread",
2441
+ session_id: "sess-stop-autoresearch-complete",
2128
2442
  }, { cwd });
2443
+ assert.equal(result.omxEventName, "stop");
2129
2444
  assert.equal(result.outputJson, null);
2130
2445
  }
2131
2446
  finally {
2132
2447
  await rm(cwd, { recursive: true, force: true });
2133
2448
  }
2134
2449
  });
2135
- it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
2136
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
2450
+ it("does not block Stop from stale root autoresearch state when the explicit session has no scoped autoresearch state", async () => {
2451
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autoresearch-"));
2137
2452
  try {
2138
2453
  const stateDir = join(cwd, ".omx", "state");
2139
- await mkdir(stateDir, { recursive: true });
2140
- await writeFile(join(stateDir, "ralph-state.json"), JSON.stringify({
2454
+ const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
2455
+ await mkdir(join(stateDir, 'sessions', 'sess-current'), { recursive: true });
2456
+ await mkdir(specDir, { recursive: true });
2457
+ await writeJson(join(stateDir, 'session.json'), { session_id: 'sess-current', cwd });
2458
+ await writeJson(join(stateDir, 'autoresearch-state.json'), {
2141
2459
  active: true,
2142
- current_phase: "executing",
2143
- }));
2460
+ mode: 'autoresearch',
2461
+ current_phase: 'executing',
2462
+ validation_mode: 'mission-validator-script',
2463
+ mission_validator_command: 'node scripts/validate.js',
2464
+ completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
2465
+ });
2144
2466
  const result = await dispatchCodexNativeHook({
2145
- hook_event_name: "Stop",
2467
+ hook_event_name: 'Stop',
2146
2468
  cwd,
2469
+ session_id: 'sess-current',
2147
2470
  }, { cwd });
2148
- assert.equal(result.omxEventName, "stop");
2149
- assert.deepEqual(result.outputJson, {
2150
- decision: "block",
2151
- reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2152
- stopReason: "ralph_executing",
2153
- systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2154
- });
2471
+ assert.equal(result.omxEventName, 'stop');
2472
+ assert.equal(result.outputJson, null);
2155
2473
  }
2156
2474
  finally {
2157
2475
  await rm(cwd, { recursive: true, force: true });
2158
2476
  }
2159
2477
  });
2160
- it("blocks Stop from session-scoped Ralph state when session.json points to another session", async () => {
2161
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-session-mismatch-"));
2478
+ it("does not block Stop solely because deep-interview is active", async () => {
2479
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
2162
2480
  try {
2163
2481
  const stateDir = join(cwd, ".omx", "state");
2164
- await mkdir(join(stateDir, "sessions", "sess-live-ralph"), { recursive: true });
2165
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-ralph" });
2166
- await writeJson(join(stateDir, "sessions", "sess-live-ralph", "ralph-state.json"), {
2482
+ await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview"), { recursive: true });
2483
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview" });
2484
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "skill-active-state.json"), {
2167
2485
  active: true,
2168
- current_phase: "executing",
2169
- session_id: "sess-live-ralph",
2486
+ skill: "deep-interview",
2487
+ phase: "planning",
2488
+ });
2489
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
2490
+ active: true,
2491
+ current_phase: "planning",
2170
2492
  });
2171
2493
  const result = await dispatchCodexNativeHook({
2172
2494
  hook_event_name: "Stop",
2173
2495
  cwd,
2174
- session_id: "sess-live-ralph",
2496
+ session_id: "sess-stop-deep-interview",
2175
2497
  }, { cwd });
2176
- assert.equal(result.omxEventName, "stop");
2177
- assert.deepEqual(result.outputJson, {
2178
- decision: "block",
2179
- reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2180
- stopReason: "ralph_executing",
2181
- systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2182
- });
2498
+ assert.equal(result.outputJson, null);
2183
2499
  }
2184
2500
  finally {
2185
2501
  await rm(cwd, { recursive: true, force: true });
2186
2502
  }
2187
2503
  });
2188
- it("does not block Stop from stale session-scoped Ralph state that belongs to another session", async () => {
2189
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralph-"));
2504
+ it("blocks Stop when deep-interview has a pending omx question obligation", async () => {
2505
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-"));
2190
2506
  try {
2191
2507
  const stateDir = join(cwd, ".omx", "state");
2192
- await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
2193
- await mkdir(join(stateDir, "sessions", "sess-stale"), { recursive: true });
2194
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
2195
- await writeJson(join(stateDir, "sessions", "sess-stale", "ralph-state.json"), {
2508
+ await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question"), { recursive: true });
2509
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question" });
2510
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "skill-active-state.json"), {
2511
+ version: 1,
2196
2512
  active: true,
2197
- current_phase: "starting",
2198
- session_id: "sess-stale",
2513
+ skill: "deep-interview",
2514
+ phase: "planning",
2515
+ session_id: "sess-stop-deep-interview-question",
2516
+ thread_id: "thread-stop-deep-interview-question",
2517
+ });
2518
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "deep-interview-state.json"), {
2519
+ active: true,
2520
+ mode: "deep-interview",
2521
+ current_phase: "intent-first",
2522
+ session_id: "sess-stop-deep-interview-question",
2523
+ thread_id: "thread-stop-deep-interview-question",
2524
+ question_enforcement: {
2525
+ obligation_id: "obligation-1",
2526
+ source: "omx-question",
2527
+ status: "pending",
2528
+ requested_at: "2026-04-19T03:20:00.000Z",
2529
+ },
2199
2530
  });
2200
2531
  const result = await dispatchCodexNativeHook({
2201
2532
  hook_event_name: "Stop",
2202
2533
  cwd,
2203
- session_id: "sess-current",
2534
+ session_id: "sess-stop-deep-interview-question",
2535
+ thread_id: "thread-stop-deep-interview-question",
2204
2536
  }, { cwd });
2205
2537
  assert.equal(result.omxEventName, "stop");
2206
- assert.equal(result.outputJson, null);
2538
+ assert.deepEqual(result.outputJson, {
2539
+ decision: "block",
2540
+ reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
2541
+ stopReason: "deep_interview_question_required",
2542
+ systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
2543
+ });
2207
2544
  }
2208
2545
  finally {
2209
2546
  await rm(cwd, { recursive: true, force: true });
2210
2547
  }
2211
2548
  });
2212
- it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
2213
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
2549
+ it("blocks Stop when a same-session deep-interview question obligation is pending even after the mode marked itself inactive", async () => {
2550
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-inactive-"));
2214
2551
  try {
2215
2552
  const stateDir = join(cwd, ".omx", "state");
2216
- await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
2217
- await writeJson(join(stateDir, "sessions", "sess-other", "ralph-state.json"), {
2553
+ await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive"), { recursive: true });
2554
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-inactive" });
2555
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive", "skill-active-state.json"), {
2556
+ version: 1,
2557
+ active: true,
2558
+ skill: "deep-interview",
2559
+ phase: "planning",
2560
+ session_id: "sess-stop-deep-interview-question-inactive",
2561
+ thread_id: "thread-stop-deep-interview-question-inactive",
2562
+ });
2563
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive", "deep-interview-state.json"), {
2564
+ active: false,
2565
+ mode: "deep-interview",
2566
+ current_phase: "intent-first",
2567
+ lifecycle_outcome: "askuserQuestion",
2568
+ run_outcome: "blocked_on_user",
2569
+ completed_at: "2026-04-19T03:20:30.000Z",
2570
+ session_id: "sess-stop-deep-interview-question-inactive",
2571
+ thread_id: "thread-stop-deep-interview-question-inactive",
2572
+ question_enforcement: {
2573
+ obligation_id: "obligation-inactive",
2574
+ source: "omx-question",
2575
+ status: "pending",
2576
+ lifecycle_outcome: "askuserQuestion",
2577
+ requested_at: "2026-04-19T03:20:00.000Z",
2578
+ },
2579
+ });
2580
+ const result = await dispatchCodexNativeHook({
2581
+ hook_event_name: "Stop",
2582
+ cwd,
2583
+ session_id: "sess-stop-deep-interview-question-inactive",
2584
+ thread_id: "thread-stop-deep-interview-question-inactive",
2585
+ }, { cwd });
2586
+ assert.equal(result.omxEventName, "stop");
2587
+ assert.deepEqual(result.outputJson, {
2588
+ decision: "block",
2589
+ reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
2590
+ stopReason: "deep_interview_question_required",
2591
+ systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
2592
+ });
2593
+ }
2594
+ finally {
2595
+ await rm(cwd, { recursive: true, force: true });
2596
+ }
2597
+ });
2598
+ it("keeps blocking pending deep-interview question Stop replays until the obligation changes", async () => {
2599
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-replay-"));
2600
+ try {
2601
+ const stateDir = join(cwd, ".omx", "state");
2602
+ await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay"), { recursive: true });
2603
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-replay" });
2604
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "skill-active-state.json"), {
2605
+ version: 1,
2606
+ active: true,
2607
+ skill: "deep-interview",
2608
+ phase: "planning",
2609
+ session_id: "sess-stop-deep-interview-question-replay",
2610
+ });
2611
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "deep-interview-state.json"), {
2612
+ active: true,
2613
+ mode: "deep-interview",
2614
+ current_phase: "intent-first",
2615
+ question_enforcement: {
2616
+ obligation_id: "obligation-replay",
2617
+ source: "omx-question",
2618
+ status: "pending",
2619
+ requested_at: "2026-04-19T03:20:00.000Z",
2620
+ },
2621
+ });
2622
+ const payload = {
2623
+ hook_event_name: "Stop",
2624
+ cwd,
2625
+ session_id: "sess-stop-deep-interview-question-replay",
2626
+ };
2627
+ const expected = {
2628
+ decision: "block",
2629
+ reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
2630
+ stopReason: "deep_interview_question_required",
2631
+ systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
2632
+ };
2633
+ const first = await dispatchCodexNativeHook(payload, { cwd });
2634
+ const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
2635
+ assert.equal(first.omxEventName, "stop");
2636
+ assert.deepEqual(first.outputJson, expected);
2637
+ assert.equal(replay.omxEventName, "stop");
2638
+ assert.deepEqual(replay.outputJson, expected);
2639
+ }
2640
+ finally {
2641
+ await rm(cwd, { recursive: true, force: true });
2642
+ }
2643
+ });
2644
+ it("does not block Stop once the deep-interview question obligation is satisfied or cleared", async () => {
2645
+ for (const status of ["satisfied", "cleared"]) {
2646
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-deep-interview-question-${status}-`));
2647
+ try {
2648
+ const stateDir = join(cwd, ".omx", "state");
2649
+ await mkdir(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`), { recursive: true });
2650
+ await writeJson(join(stateDir, "session.json"), { session_id: `sess-stop-deep-interview-question-${status}` });
2651
+ await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "skill-active-state.json"), {
2652
+ version: 1,
2653
+ active: true,
2654
+ skill: "deep-interview",
2655
+ phase: "planning",
2656
+ session_id: `sess-stop-deep-interview-question-${status}`,
2657
+ });
2658
+ await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "deep-interview-state.json"), {
2659
+ active: true,
2660
+ mode: "deep-interview",
2661
+ current_phase: "intent-first",
2662
+ question_enforcement: {
2663
+ obligation_id: `obligation-${status}`,
2664
+ source: "omx-question",
2665
+ status,
2666
+ requested_at: "2026-04-19T03:20:00.000Z",
2667
+ ...(status === "satisfied"
2668
+ ? { question_id: "question-1", satisfied_at: "2026-04-19T03:21:00.000Z" }
2669
+ : { cleared_at: "2026-04-19T03:21:00.000Z", clear_reason: "error" }),
2670
+ },
2671
+ });
2672
+ const result = await dispatchCodexNativeHook({
2673
+ hook_event_name: "Stop",
2674
+ cwd,
2675
+ session_id: `sess-stop-deep-interview-question-${status}`,
2676
+ }, { cwd });
2677
+ assert.equal(result.omxEventName, "stop");
2678
+ assert.equal(result.outputJson, null);
2679
+ }
2680
+ finally {
2681
+ await rm(cwd, { recursive: true, force: true });
2682
+ }
2683
+ }
2684
+ });
2685
+ it("ignores pending deep-interview question obligations from another session", async () => {
2686
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-foreign-session-"));
2687
+ try {
2688
+ const stateDir = join(cwd, ".omx", "state");
2689
+ await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
2690
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
2691
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
2692
+ await writeJson(join(stateDir, "sessions", "sess-other", "skill-active-state.json"), {
2693
+ version: 1,
2694
+ active: true,
2695
+ skill: "deep-interview",
2696
+ phase: "planning",
2697
+ session_id: "sess-other",
2698
+ });
2699
+ await writeJson(join(stateDir, "sessions", "sess-other", "deep-interview-state.json"), {
2700
+ active: true,
2701
+ mode: "deep-interview",
2702
+ current_phase: "intent-first",
2703
+ question_enforcement: {
2704
+ obligation_id: "obligation-foreign",
2705
+ source: "omx-question",
2706
+ status: "pending",
2707
+ requested_at: "2026-04-19T03:20:00.000Z",
2708
+ },
2709
+ });
2710
+ const result = await dispatchCodexNativeHook({
2711
+ hook_event_name: "Stop",
2712
+ cwd,
2713
+ session_id: "sess-current",
2714
+ }, { cwd });
2715
+ assert.equal(result.omxEventName, "stop");
2716
+ assert.equal(result.outputJson, null);
2717
+ }
2718
+ finally {
2719
+ await rm(cwd, { recursive: true, force: true });
2720
+ }
2721
+ });
2722
+ it("blocks a new same-session deep-interview question obligation even after an earlier round was satisfied", async () => {
2723
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-next-round-"));
2724
+ try {
2725
+ const stateDir = join(cwd, ".omx", "state");
2726
+ await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round"), { recursive: true });
2727
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-next-round" });
2728
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "skill-active-state.json"), {
2729
+ version: 1,
2730
+ active: true,
2731
+ skill: "deep-interview",
2732
+ phase: "planning",
2733
+ session_id: "sess-stop-deep-interview-question-next-round",
2734
+ });
2735
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "deep-interview-state.json"), {
2736
+ active: true,
2737
+ mode: "deep-interview",
2738
+ current_phase: "intent-first",
2739
+ question_enforcement: {
2740
+ obligation_id: "obligation-next-round",
2741
+ source: "omx-question",
2742
+ status: "pending",
2743
+ requested_at: "2026-04-19T03:22:00.000Z",
2744
+ question_id: "question-old-round",
2745
+ satisfied_at: "2026-04-19T03:21:00.000Z",
2746
+ },
2747
+ });
2748
+ const result = await dispatchCodexNativeHook({
2749
+ hook_event_name: "Stop",
2750
+ cwd,
2751
+ session_id: "sess-stop-deep-interview-question-next-round",
2752
+ }, { cwd });
2753
+ assert.equal(result.omxEventName, "stop");
2754
+ assert.deepEqual(result.outputJson, {
2755
+ decision: "block",
2756
+ reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
2757
+ stopReason: "deep_interview_question_required",
2758
+ systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
2759
+ });
2760
+ }
2761
+ finally {
2762
+ await rm(cwd, { recursive: true, force: true });
2763
+ }
2764
+ });
2765
+ it("ignores root skill-active fallback from a different thread when evaluating Stop", async () => {
2766
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-thread-"));
2767
+ try {
2768
+ const stateDir = join(cwd, ".omx", "state");
2769
+ await mkdir(stateDir, { recursive: true });
2770
+ await writeJson(join(stateDir, "skill-active-state.json"), {
2771
+ active: true,
2772
+ skill: "deep-interview",
2773
+ phase: "planning",
2774
+ session_id: "",
2775
+ thread_id: "other-thread",
2776
+ });
2777
+ const result = await dispatchCodexNativeHook({
2778
+ hook_event_name: "Stop",
2779
+ cwd,
2780
+ session_id: "sess-stop-main",
2781
+ thread_id: "main-thread",
2782
+ }, { cwd });
2783
+ assert.equal(result.outputJson, null);
2784
+ }
2785
+ finally {
2786
+ await rm(cwd, { recursive: true, force: true });
2787
+ }
2788
+ });
2789
+ it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
2790
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
2791
+ try {
2792
+ const stateDir = join(cwd, ".omx", "state");
2793
+ await mkdir(stateDir, { recursive: true });
2794
+ await writeFile(join(stateDir, "ralph-state.json"), JSON.stringify({
2795
+ active: true,
2796
+ current_phase: "executing",
2797
+ }));
2798
+ const result = await dispatchCodexNativeHook({
2799
+ hook_event_name: "Stop",
2800
+ cwd,
2801
+ }, { cwd });
2802
+ assert.equal(result.omxEventName, "stop");
2803
+ assert.deepEqual(result.outputJson, {
2804
+ decision: "block",
2805
+ reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2806
+ stopReason: "ralph_executing",
2807
+ systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2808
+ });
2809
+ }
2810
+ finally {
2811
+ await rm(cwd, { recursive: true, force: true });
2812
+ }
2813
+ });
2814
+ it("blocks Stop from session-scoped Ralph state when session.json points to another session", async () => {
2815
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-session-mismatch-"));
2816
+ try {
2817
+ const stateDir = join(cwd, ".omx", "state");
2818
+ await mkdir(join(stateDir, "sessions", "sess-live-ralph"), { recursive: true });
2819
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-ralph" });
2820
+ await writeJson(join(stateDir, "sessions", "sess-live-ralph", "ralph-state.json"), {
2821
+ active: true,
2822
+ current_phase: "executing",
2823
+ session_id: "sess-live-ralph",
2824
+ });
2825
+ const result = await dispatchCodexNativeHook({
2826
+ hook_event_name: "Stop",
2827
+ cwd,
2828
+ session_id: "sess-live-ralph",
2829
+ }, { cwd });
2830
+ assert.equal(result.omxEventName, "stop");
2831
+ assert.deepEqual(result.outputJson, {
2832
+ decision: "block",
2833
+ reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2834
+ stopReason: "ralph_executing",
2835
+ systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2836
+ });
2837
+ }
2838
+ finally {
2839
+ await rm(cwd, { recursive: true, force: true });
2840
+ }
2841
+ });
2842
+ it("does not block Stop from stale session-scoped Ralph state that belongs to another session", async () => {
2843
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralph-"));
2844
+ try {
2845
+ const stateDir = join(cwd, ".omx", "state");
2846
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
2847
+ await mkdir(join(stateDir, "sessions", "sess-stale"), { recursive: true });
2848
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
2849
+ await writeJson(join(stateDir, "sessions", "sess-stale", "ralph-state.json"), {
2850
+ active: true,
2851
+ current_phase: "starting",
2852
+ session_id: "sess-stale",
2853
+ });
2854
+ const result = await dispatchCodexNativeHook({
2855
+ hook_event_name: "Stop",
2856
+ cwd,
2857
+ session_id: "sess-current",
2858
+ }, { cwd });
2859
+ assert.equal(result.omxEventName, "stop");
2860
+ assert.equal(result.outputJson, null);
2861
+ }
2862
+ finally {
2863
+ await rm(cwd, { recursive: true, force: true });
2864
+ }
2865
+ });
2866
+ it("does not block Stop from stale current-session Ralph state when session.json points to a dead owner", async () => {
2867
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-current-session-ralph-"));
2868
+ try {
2869
+ const stateDir = join(cwd, ".omx", "state");
2870
+ await mkdir(join(stateDir, "sessions", "sess-dead"), { recursive: true });
2871
+ await writeJson(join(stateDir, "session.json"), {
2872
+ session_id: "sess-dead",
2873
+ cwd,
2874
+ pid: Number.MAX_SAFE_INTEGER,
2875
+ started_at: "2026-01-01T00:00:00.000Z",
2876
+ });
2877
+ await writeJson(join(stateDir, "sessions", "sess-dead", "ralph-state.json"), {
2878
+ active: true,
2879
+ current_phase: "verifying",
2880
+ session_id: "sess-dead",
2881
+ });
2882
+ await writeJson(join(stateDir, "skill-active-state.json"), {
2883
+ active: true,
2884
+ skill: "team",
2885
+ phase: "team-exec",
2886
+ active_skills: [{ skill: "team", phase: "team-exec", active: true, session_id: "sess-dead" }],
2887
+ });
2888
+ await writeJson(join(stateDir, "native-stop-state.json"), {
2889
+ sessions: {
2890
+ "sess-dead": {
2891
+ last_signature: "ralph-stop|sess-dead|thread-1|no-message|verifying",
2892
+ updated_at: "2026-04-20T21:00:00.000Z",
2893
+ },
2894
+ },
2895
+ });
2896
+ const result = await dispatchCodexNativeHook({
2897
+ hook_event_name: "Stop",
2898
+ cwd,
2899
+ session_id: "sess-dead",
2900
+ thread_id: "thread-1",
2901
+ stop_hook_active: true,
2902
+ }, { cwd });
2903
+ assert.equal(result.omxEventName, "stop");
2904
+ assert.equal(result.outputJson, null);
2905
+ }
2906
+ finally {
2907
+ await rm(cwd, { recursive: true, force: true });
2908
+ }
2909
+ });
2910
+ it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
2911
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
2912
+ try {
2913
+ const stateDir = join(cwd, ".omx", "state");
2914
+ await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
2915
+ await writeJson(join(stateDir, "sessions", "sess-other", "ralph-state.json"), {
2218
2916
  active: true,
2219
2917
  current_phase: "starting",
2220
2918
  session_id: "sess-other",
@@ -2346,6 +3044,91 @@ esac
2346
3044
  await rm(cwd, { recursive: true, force: true });
2347
3045
  }
2348
3046
  });
3047
+ it("lets dispatcher dedupe identical native stop hook replays after Stop payload normalization", async () => {
3048
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-hook-dedupe-"));
3049
+ const previousOmxSessionId = process.env.OMX_SESSION_ID;
3050
+ try {
3051
+ const stateDir = join(cwd, ".omx", "state");
3052
+ await mkdir(join(stateDir, "sessions", "sess-stop-ralph-hook-dedupe"), { recursive: true });
3053
+ await writeHookCounterPlugin(cwd);
3054
+ await writeFile(join(stateDir, "sessions", "sess-stop-ralph-hook-dedupe", "ralph-state.json"), JSON.stringify({
3055
+ active: true,
3056
+ current_phase: "executing",
3057
+ session_id: "sess-stop-ralph-hook-dedupe",
3058
+ }));
3059
+ process.env.OMX_SESSION_ID = "sess-stop-ralph-hook-dedupe";
3060
+ const payload = {
3061
+ hook_event_name: "Stop",
3062
+ cwd,
3063
+ session_id: "sess-stop-ralph-hook-dedupe",
3064
+ thread_id: "thread-stop-ralph-hook-dedupe",
3065
+ turn_id: "turn-stop-ralph-hook-dedupe-1",
3066
+ last_assistant_message: "Next active targets:\n\n1. scheduler integration\n\nI am continuing.",
3067
+ };
3068
+ await dispatchCodexNativeHook(payload, { cwd });
3069
+ await dispatchCodexNativeHook({
3070
+ ...payload,
3071
+ stop_hook_active: true,
3072
+ }, { cwd });
3073
+ const marker = JSON.parse(await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"));
3074
+ assert.equal(marker.count, 1);
3075
+ }
3076
+ finally {
3077
+ if (typeof previousOmxSessionId === "string")
3078
+ process.env.OMX_SESSION_ID = previousOmxSessionId;
3079
+ else
3080
+ delete process.env.OMX_SESSION_ID;
3081
+ await rm(cwd, { recursive: true, force: true });
3082
+ }
3083
+ });
3084
+ it("preserves per-turn native stop hook delivery even when stop_hook_active remains true", async () => {
3085
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-hook-refire-"));
3086
+ const previousOmxSessionId = process.env.OMX_SESSION_ID;
3087
+ try {
3088
+ const stateDir = join(cwd, ".omx", "state");
3089
+ await mkdir(join(stateDir, "sessions", "sess-stop-ralph-hook-refire"), { recursive: true });
3090
+ await writeHookCounterPlugin(cwd);
3091
+ await writeFile(join(stateDir, "sessions", "sess-stop-ralph-hook-refire", "ralph-state.json"), JSON.stringify({
3092
+ active: true,
3093
+ current_phase: "executing",
3094
+ session_id: "sess-stop-ralph-hook-refire",
3095
+ }));
3096
+ process.env.OMX_SESSION_ID = "sess-stop-ralph-hook-refire";
3097
+ const payload = {
3098
+ hook_event_name: "Stop",
3099
+ cwd,
3100
+ session_id: "sess-stop-ralph-hook-refire",
3101
+ thread_id: "thread-stop-ralph-hook-refire",
3102
+ turn_id: "turn-stop-ralph-hook-refire-1",
3103
+ last_assistant_message: "Continuing current task.",
3104
+ };
3105
+ await dispatchCodexNativeHook(payload, { cwd });
3106
+ await dispatchCodexNativeHook({
3107
+ ...payload,
3108
+ turn_id: "turn-stop-ralph-hook-refire-2",
3109
+ stop_hook_active: true,
3110
+ }, { cwd });
3111
+ await writeFile(join(stateDir, "sessions", "sess-stop-ralph-hook-refire", "ralph-state.json"), JSON.stringify({
3112
+ active: true,
3113
+ current_phase: "executing",
3114
+ session_id: "sess-stop-ralph-hook-refire",
3115
+ }));
3116
+ await dispatchCodexNativeHook({
3117
+ ...payload,
3118
+ turn_id: "turn-stop-ralph-hook-refire-3",
3119
+ stop_hook_active: true,
3120
+ }, { cwd });
3121
+ const marker = JSON.parse(await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"));
3122
+ assert.equal(marker.count, 3);
3123
+ }
3124
+ finally {
3125
+ if (typeof previousOmxSessionId === "string")
3126
+ process.env.OMX_SESSION_ID = previousOmxSessionId;
3127
+ else
3128
+ delete process.env.OMX_SESSION_ID;
3129
+ await rm(cwd, { recursive: true, force: true });
3130
+ }
3131
+ });
2349
3132
  it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
2350
3133
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
2351
3134
  try {
@@ -2446,6 +3229,51 @@ esac
2446
3229
  await rm(cwd, { recursive: true, force: true });
2447
3230
  }
2448
3231
  });
3232
+ it("dedupes native stop hook replay across owner launch SessionStart reconciliation drift", async () => {
3233
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-dispatch-session-drift-"));
3234
+ try {
3235
+ const stateDir = join(cwd, ".omx", "state");
3236
+ await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
3237
+ await writeHookCounterPlugin(cwd);
3238
+ process.env.OMX_SESSION_ID = "omx-canonical";
3239
+ await writeSessionStart(cwd, "omx-canonical");
3240
+ await writeJson(join(stateDir, "sessions", "omx-canonical", "ralph-state.json"), {
3241
+ active: true,
3242
+ current_phase: "executing",
3243
+ session_id: "omx-canonical",
3244
+ });
3245
+ await dispatchCodexNativeHook({
3246
+ hook_event_name: "SessionStart",
3247
+ cwd,
3248
+ session_id: "codex-native-new",
3249
+ }, { cwd, sessionOwnerPid: process.pid });
3250
+ await dispatchCodexNativeHook({
3251
+ hook_event_name: "Stop",
3252
+ cwd,
3253
+ session_id: "codex-native-new",
3254
+ thread_id: "thread-stop-hook-drift",
3255
+ turn_id: "turn-stop-hook-drift-1",
3256
+ last_assistant_message: "Keep going and finish the cleanup.",
3257
+ }, { cwd });
3258
+ await dispatchCodexNativeHook({
3259
+ hook_event_name: "Stop",
3260
+ cwd,
3261
+ session_id: "omx-canonical",
3262
+ thread_id: "thread-stop-hook-drift",
3263
+ turn_id: "turn-stop-hook-drift-1",
3264
+ stop_hook_active: true,
3265
+ last_assistant_message: "Keep going and finish the cleanup.",
3266
+ }, { cwd });
3267
+ const marker = JSON.parse(await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"));
3268
+ assert.equal(marker.count, 1);
3269
+ const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
3270
+ assert.equal(sessionState.session_id, "omx-canonical");
3271
+ assert.equal(sessionState.native_session_id, "codex-native-new");
3272
+ }
3273
+ finally {
3274
+ await rm(cwd, { recursive: true, force: true });
3275
+ }
3276
+ });
2449
3277
  it("re-fires native auto-nudge for a later fresh Stop reply even when stop_hook_active is true", async () => {
2450
3278
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-refire-"));
2451
3279
  try {
@@ -2903,61 +3731,52 @@ esac
2903
3731
  await rm(cwd, { recursive: true, force: true });
2904
3732
  }
2905
3733
  });
2906
- it("re-blocks active execution modes on repeated Stop hooks", async () => {
2907
- const cases = [
2908
- {
2909
- mode: "autopilot",
2910
- phase: "execution",
2911
- reason: "OMX autopilot is still active (phase: execution); continue the task and gather fresh verification evidence before stopping.",
2912
- },
2913
- {
2914
- mode: "ultrawork",
2915
- phase: "executing",
2916
- reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
2917
- },
2918
- {
2919
- mode: "ultraqa",
2920
- phase: "diagnose",
2921
- reason: "OMX ultraqa is still active (phase: diagnose); continue the task and gather fresh verification evidence before stopping.",
2922
- },
2923
- ];
2924
- for (const testCase of cases) {
2925
- const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-${testCase.mode}-repeat-`));
2926
- try {
2927
- const stateDir = join(cwd, ".omx", "state");
2928
- await mkdir(stateDir, { recursive: true });
2929
- await writeJson(join(stateDir, `${testCase.mode}-state.json`), {
2930
- active: true,
2931
- current_phase: testCase.phase,
2932
- });
2933
- await dispatchCodexNativeHook({
2934
- hook_event_name: "Stop",
2935
- cwd,
2936
- session_id: `sess-stop-${testCase.mode}-repeat`,
2937
- thread_id: `thread-stop-${testCase.mode}-repeat`,
2938
- turn_id: `turn-stop-${testCase.mode}-repeat-1`,
2939
- }, { cwd });
2940
- const repeated = await dispatchCodexNativeHook({
2941
- hook_event_name: "Stop",
2942
- cwd,
2943
- session_id: `sess-stop-${testCase.mode}-repeat`,
2944
- thread_id: `thread-stop-${testCase.mode}-repeat`,
2945
- turn_id: `turn-stop-${testCase.mode}-repeat-1`,
2946
- stop_hook_active: true,
2947
- }, { cwd });
2948
- assert.equal(repeated.omxEventName, "stop");
2949
- assert.deepEqual(repeated.outputJson, {
2950
- decision: "block",
2951
- reason: testCase.reason,
2952
- stopReason: `${testCase.mode}_${testCase.phase}`,
2953
- systemMessage: `OMX ${testCase.mode} is still active (phase: ${testCase.phase}).`,
2954
- });
2955
- }
2956
- finally {
2957
- await rm(cwd, { recursive: true, force: true });
2958
- }
2959
- }
2960
- });
3734
+ it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
3735
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
3736
+ try {
3737
+ const stateDir = join(cwd, ".omx", "state");
3738
+ await mkdir(stateDir, { recursive: true });
3739
+ await writeJson(join(stateDir, "ultrawork-state.json"), {
3740
+ active: true,
3741
+ current_phase: "executing",
3742
+ });
3743
+ const first = await dispatchCodexNativeHook({
3744
+ hook_event_name: "Stop",
3745
+ cwd,
3746
+ session_id: "sess-stop-ultrawork-repeat",
3747
+ thread_id: "thread-stop-ultrawork-repeat",
3748
+ turn_id: "turn-stop-ultrawork-repeat-1",
3749
+ }, { cwd });
3750
+ const repeated = await dispatchCodexNativeHook({
3751
+ hook_event_name: "Stop",
3752
+ cwd,
3753
+ session_id: "sess-stop-ultrawork-repeat",
3754
+ thread_id: "thread-stop-ultrawork-repeat",
3755
+ turn_id: "turn-stop-ultrawork-repeat-1",
3756
+ stop_hook_active: true,
3757
+ }, { cwd });
3758
+ const fresh = await dispatchCodexNativeHook({
3759
+ hook_event_name: "Stop",
3760
+ cwd,
3761
+ session_id: "sess-stop-ultrawork-repeat",
3762
+ thread_id: "thread-stop-ultrawork-repeat",
3763
+ turn_id: "turn-stop-ultrawork-repeat-2",
3764
+ stop_hook_active: true,
3765
+ }, { cwd });
3766
+ assert.equal(first.omxEventName, "stop");
3767
+ assert.deepEqual(repeated.outputJson, null);
3768
+ assert.equal(fresh.omxEventName, "stop");
3769
+ assert.deepEqual(fresh.outputJson, {
3770
+ decision: "block",
3771
+ reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
3772
+ stopReason: "ultrawork_executing",
3773
+ systemMessage: "OMX ultrawork is still active (phase: executing).",
3774
+ });
3775
+ }
3776
+ finally {
3777
+ await rm(cwd, { recursive: true, force: true });
3778
+ }
3779
+ });
2961
3780
  it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
2962
3781
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
2963
3782
  try {
@@ -3142,4 +3961,518 @@ esac
3142
3961
  }
3143
3962
  });
3144
3963
  });
3964
+ // ---------------------------------------------------------------------------
3965
+ // Triage layer integration tests
3966
+ // ---------------------------------------------------------------------------
3967
+ describe("codex native hook triage integration", () => {
3968
+ const priorCodexHome = process.env.CODEX_HOME;
3969
+ beforeEach(() => {
3970
+ resetTriageConfigCache();
3971
+ });
3972
+ afterEach(() => {
3973
+ if (typeof priorCodexHome === "string")
3974
+ process.env.CODEX_HOME = priorCodexHome;
3975
+ else
3976
+ delete process.env.CODEX_HOME;
3977
+ resetTriageConfigCache();
3978
+ });
3979
+ // ── Group 1: Keyword bypass (triage must NOT run) ────────────────────────
3980
+ it("does not inject triage advisory for $ralplan keyword prompts", async () => {
3981
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-ralplan-"));
3982
+ try {
3983
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
3984
+ const result = await dispatchCodexNativeHook({
3985
+ hook_event_name: "UserPromptSubmit",
3986
+ cwd,
3987
+ session_id: "triage-kw-ralplan-1",
3988
+ thread_id: "thread-triage-kw-1",
3989
+ turn_id: "turn-triage-kw-1",
3990
+ prompt: "$ralplan implement issue #1307",
3991
+ }, { cwd });
3992
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
3993
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
3994
+ assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
3995
+ assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
3996
+ assert.doesNotMatch(additionalContext, /visual\/style request/);
3997
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-ralplan-1", "prompt-routing-state.json");
3998
+ assert.equal(existsSync(stateFile), false);
3999
+ }
4000
+ finally {
4001
+ await rm(cwd, { recursive: true, force: true });
4002
+ }
4003
+ });
4004
+ it("does not inject triage advisory for autopilot keyword prompts", async () => {
4005
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-autopilot-"));
4006
+ try {
4007
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4008
+ const result = await dispatchCodexNativeHook({
4009
+ hook_event_name: "UserPromptSubmit",
4010
+ cwd,
4011
+ session_id: "triage-kw-autopilot-1",
4012
+ thread_id: "thread-triage-kw-ap-1",
4013
+ turn_id: "turn-triage-kw-ap-1",
4014
+ prompt: "$autopilot build this",
4015
+ }, { cwd });
4016
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4017
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
4018
+ assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
4019
+ assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
4020
+ assert.doesNotMatch(additionalContext, /visual\/style request/);
4021
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-autopilot-1", "prompt-routing-state.json");
4022
+ assert.equal(existsSync(stateFile), false);
4023
+ }
4024
+ finally {
4025
+ await rm(cwd, { recursive: true, force: true });
4026
+ }
4027
+ });
4028
+ // ── Group 2: HEAVY injection ─────────────────────────────────────────────
4029
+ it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
4030
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));
4031
+ try {
4032
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4033
+ const result = await dispatchCodexNativeHook({
4034
+ hook_event_name: "UserPromptSubmit",
4035
+ cwd,
4036
+ session_id: "triage-heavy-1",
4037
+ thread_id: "thread-triage-heavy-1",
4038
+ turn_id: "turn-triage-heavy-1",
4039
+ prompt: "add dark mode toggle to the settings page",
4040
+ }, { cwd });
4041
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4042
+ assert.match(additionalContext, /multi-step goal with no workflow keyword/);
4043
+ assert.match(additionalContext, /Prefer the existing autopilot-style workflow/);
4044
+ // skill-active-state.json must NOT be written (triage is advisory only)
4045
+ assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
4046
+ // prompt-routing-state.json must be written with lane=HEAVY
4047
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-heavy-1", "prompt-routing-state.json");
4048
+ assert.equal(existsSync(stateFile), true);
4049
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
4050
+ assert.equal(state.version, 1);
4051
+ assert.equal(state.last_triage?.lane, "HEAVY");
4052
+ assert.equal(state.last_triage?.destination, "autopilot");
4053
+ assert.equal(state.suppress_followup, true);
4054
+ }
4055
+ finally {
4056
+ await rm(cwd, { recursive: true, force: true });
4057
+ }
4058
+ });
4059
+ // ── Group 3: LIGHT/explore ────────────────────────────────────────────────
4060
+ it("injects LIGHT/explore advisory and writes state for a question-shaped prompt", async () => {
4061
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-explore-"));
4062
+ try {
4063
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4064
+ const result = await dispatchCodexNativeHook({
4065
+ hook_event_name: "UserPromptSubmit",
4066
+ cwd,
4067
+ session_id: "triage-explore-1",
4068
+ thread_id: "thread-triage-explore-1",
4069
+ turn_id: "turn-triage-explore-1",
4070
+ prompt: "explain this function",
4071
+ }, { cwd });
4072
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4073
+ assert.match(additionalContext, /read-only\/question-shaped/);
4074
+ assert.match(additionalContext, /Prefer the explore role surface/);
4075
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-explore-1", "prompt-routing-state.json");
4076
+ assert.equal(existsSync(stateFile), true);
4077
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
4078
+ assert.equal(state.last_triage?.lane, "LIGHT");
4079
+ assert.equal(state.last_triage?.destination, "explore");
4080
+ assert.equal(state.suppress_followup, true);
4081
+ }
4082
+ finally {
4083
+ await rm(cwd, { recursive: true, force: true });
4084
+ }
4085
+ });
4086
+ // ── Group 4: LIGHT/executor ───────────────────────────────────────────────
4087
+ it("injects LIGHT/executor advisory and writes state for a narrow edit-shaped prompt", async () => {
4088
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-executor-"));
4089
+ try {
4090
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4091
+ const result = await dispatchCodexNativeHook({
4092
+ hook_event_name: "UserPromptSubmit",
4093
+ cwd,
4094
+ session_id: "triage-executor-1",
4095
+ thread_id: "thread-triage-executor-1",
4096
+ turn_id: "turn-triage-executor-1",
4097
+ prompt: "fix typo in src/foo.ts",
4098
+ }, { cwd });
4099
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4100
+ assert.match(additionalContext, /narrow edit-shaped/);
4101
+ assert.match(additionalContext, /Prefer the executor role surface/);
4102
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-executor-1", "prompt-routing-state.json");
4103
+ assert.equal(existsSync(stateFile), true);
4104
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
4105
+ assert.equal(state.last_triage?.lane, "LIGHT");
4106
+ assert.equal(state.last_triage?.destination, "executor");
4107
+ }
4108
+ finally {
4109
+ await rm(cwd, { recursive: true, force: true });
4110
+ }
4111
+ });
4112
+ // ── Group 5: LIGHT/designer ───────────────────────────────────────────────
4113
+ it("injects LIGHT/designer advisory and writes state for a visual/style prompt", async () => {
4114
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-designer-"));
4115
+ try {
4116
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4117
+ const result = await dispatchCodexNativeHook({
4118
+ hook_event_name: "UserPromptSubmit",
4119
+ cwd,
4120
+ session_id: "triage-designer-1",
4121
+ thread_id: "thread-triage-designer-1",
4122
+ turn_id: "turn-triage-designer-1",
4123
+ prompt: "make the button blue",
4124
+ }, { cwd });
4125
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4126
+ assert.match(additionalContext, /visual\/style request/);
4127
+ assert.match(additionalContext, /Prefer the designer role surface/);
4128
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-designer-1", "prompt-routing-state.json");
4129
+ assert.equal(existsSync(stateFile), true);
4130
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
4131
+ assert.equal(state.last_triage?.lane, "LIGHT");
4132
+ assert.equal(state.last_triage?.destination, "designer");
4133
+ }
4134
+ finally {
4135
+ await rm(cwd, { recursive: true, force: true });
4136
+ }
4137
+ });
4138
+ // ── Group 6: PASS (no triage injection, no state) ────────────────────────
4139
+ it("produces no triage advisory and no state for trivial greeting prompts", async () => {
4140
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-hello-"));
4141
+ try {
4142
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4143
+ const result = await dispatchCodexNativeHook({
4144
+ hook_event_name: "UserPromptSubmit",
4145
+ cwd,
4146
+ session_id: "triage-pass-hello-1",
4147
+ thread_id: "thread-triage-pass-1",
4148
+ turn_id: "turn-triage-pass-1",
4149
+ prompt: "hello",
4150
+ }, { cwd });
4151
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4152
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
4153
+ assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
4154
+ assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
4155
+ assert.doesNotMatch(additionalContext, /visual\/style request/);
4156
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-hello-1", "prompt-routing-state.json");
4157
+ assert.equal(existsSync(stateFile), false);
4158
+ }
4159
+ finally {
4160
+ await rm(cwd, { recursive: true, force: true });
4161
+ }
4162
+ });
4163
+ it("produces no triage advisory and no state for ambiguous short prompts", async () => {
4164
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-short-"));
4165
+ try {
4166
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4167
+ const result = await dispatchCodexNativeHook({
4168
+ hook_event_name: "UserPromptSubmit",
4169
+ cwd,
4170
+ session_id: "triage-pass-short-1",
4171
+ thread_id: "thread-triage-pass-short-1",
4172
+ turn_id: "turn-triage-pass-short-1",
4173
+ prompt: "fix the thing",
4174
+ }, { cwd });
4175
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4176
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
4177
+ assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
4178
+ assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
4179
+ assert.doesNotMatch(additionalContext, /visual\/style request/);
4180
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-short-1", "prompt-routing-state.json");
4181
+ assert.equal(existsSync(stateFile), false);
4182
+ }
4183
+ finally {
4184
+ await rm(cwd, { recursive: true, force: true });
4185
+ }
4186
+ });
4187
+ // ── Group 7: Turn-2 suppression (same session across two invocations) ────
4188
+ it("suppresses HEAVY triage re-injection on a short follow-up in the same session", async () => {
4189
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-heavy-"));
4190
+ const sessionId = "triage-suppress-heavy-1";
4191
+ try {
4192
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4193
+ // Turn 1: HEAVY fires
4194
+ const turn1 = await dispatchCodexNativeHook({
4195
+ hook_event_name: "UserPromptSubmit",
4196
+ cwd,
4197
+ session_id: sessionId,
4198
+ thread_id: "thread-suppress-heavy-1",
4199
+ turn_id: "turn-suppress-heavy-1",
4200
+ prompt: "add dark mode toggle to the settings page",
4201
+ }, { cwd });
4202
+ const ctx1 = String(turn1.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4203
+ assert.match(ctx1, /multi-step goal with no workflow keyword/);
4204
+ // Turn 2: short follow-up — triage suppressed
4205
+ const turn2 = await dispatchCodexNativeHook({
4206
+ hook_event_name: "UserPromptSubmit",
4207
+ cwd,
4208
+ session_id: sessionId,
4209
+ thread_id: "thread-suppress-heavy-1",
4210
+ turn_id: "turn-suppress-heavy-2",
4211
+ prompt: "yes, settings page",
4212
+ }, { cwd });
4213
+ const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4214
+ assert.doesNotMatch(ctx2, /multi-step goal/);
4215
+ }
4216
+ finally {
4217
+ await rm(cwd, { recursive: true, force: true });
4218
+ }
4219
+ });
4220
+ it("suppresses LIGHT/explore triage re-injection on a short follow-up in the same session", async () => {
4221
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-explore-"));
4222
+ const sessionId = "triage-suppress-explore-1";
4223
+ try {
4224
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4225
+ // Turn 1: LIGHT/explore fires
4226
+ await dispatchCodexNativeHook({
4227
+ hook_event_name: "UserPromptSubmit",
4228
+ cwd,
4229
+ session_id: sessionId,
4230
+ thread_id: "thread-suppress-explore-1",
4231
+ turn_id: "turn-suppress-explore-1",
4232
+ prompt: "explain this function",
4233
+ }, { cwd });
4234
+ // Turn 2: short follow-up — no duplicate LIGHT injection
4235
+ const turn2 = await dispatchCodexNativeHook({
4236
+ hook_event_name: "UserPromptSubmit",
4237
+ cwd,
4238
+ session_id: sessionId,
4239
+ thread_id: "thread-suppress-explore-1",
4240
+ turn_id: "turn-suppress-explore-2",
4241
+ prompt: "the auth helper",
4242
+ }, { cwd });
4243
+ const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4244
+ assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
4245
+ }
4246
+ finally {
4247
+ await rm(cwd, { recursive: true, force: true });
4248
+ }
4249
+ });
4250
+ // ── Group 8: First-turn PASS does NOT block later triage ─────────────────
4251
+ it("still applies triage on turn 2 when turn 1 was a PASS with no state written", async () => {
4252
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-then-light-"));
4253
+ const sessionId = "triage-pass-then-light-1";
4254
+ try {
4255
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4256
+ // Turn 1: PASS — no state written
4257
+ await dispatchCodexNativeHook({
4258
+ hook_event_name: "UserPromptSubmit",
4259
+ cwd,
4260
+ session_id: sessionId,
4261
+ thread_id: "thread-pass-then-light-1",
4262
+ turn_id: "turn-pass-then-light-1",
4263
+ prompt: "hello",
4264
+ }, { cwd });
4265
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json")), false);
4266
+ // Turn 2: LIGHT/executor should fire normally
4267
+ const turn2 = await dispatchCodexNativeHook({
4268
+ hook_event_name: "UserPromptSubmit",
4269
+ cwd,
4270
+ session_id: sessionId,
4271
+ thread_id: "thread-pass-then-light-1",
4272
+ turn_id: "turn-pass-then-light-2",
4273
+ prompt: "fix typo in src/foo.ts",
4274
+ }, { cwd });
4275
+ const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4276
+ assert.match(ctx2, /narrow edit-shaped/);
4277
+ }
4278
+ finally {
4279
+ await rm(cwd, { recursive: true, force: true });
4280
+ }
4281
+ });
4282
+ // ── Group 9: Opt-out forces PASS ─────────────────────────────────────────
4283
+ it("produces no triage advisory when prompt contains 'just chat' opt-out", async () => {
4284
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-chat-"));
4285
+ try {
4286
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4287
+ const result = await dispatchCodexNativeHook({
4288
+ hook_event_name: "UserPromptSubmit",
4289
+ cwd,
4290
+ session_id: "triage-optout-chat-1",
4291
+ thread_id: "thread-optout-chat-1",
4292
+ turn_id: "turn-optout-chat-1",
4293
+ prompt: "add dark mode toggle to the settings page, but just chat about it",
4294
+ }, { cwd });
4295
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4296
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
4297
+ assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
4298
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-chat-1", "prompt-routing-state.json");
4299
+ assert.equal(existsSync(stateFile), false);
4300
+ }
4301
+ finally {
4302
+ await rm(cwd, { recursive: true, force: true });
4303
+ }
4304
+ });
4305
+ it("produces no triage advisory when prompt contains 'no workflow' opt-out", async () => {
4306
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-noworkflow-"));
4307
+ try {
4308
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4309
+ const result = await dispatchCodexNativeHook({
4310
+ hook_event_name: "UserPromptSubmit",
4311
+ cwd,
4312
+ session_id: "triage-optout-noworkflow-1",
4313
+ thread_id: "thread-optout-noworkflow-1",
4314
+ turn_id: "turn-optout-noworkflow-1",
4315
+ prompt: "make the button blue, no workflow",
4316
+ }, { cwd });
4317
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4318
+ assert.doesNotMatch(additionalContext, /visual\/style request/);
4319
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-noworkflow-1", "prompt-routing-state.json");
4320
+ assert.equal(existsSync(stateFile), false);
4321
+ }
4322
+ finally {
4323
+ await rm(cwd, { recursive: true, force: true });
4324
+ }
4325
+ });
4326
+ // ── Group 10: Keyword on follow-up turn wins cleanly ─────────────────────
4327
+ it("keyword on turn 2 suppresses triage and writes no triage state", async () => {
4328
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-kw-followup-"));
4329
+ const sessionId = "triage-kw-followup-1";
4330
+ try {
4331
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4332
+ // Turn 1: neutral prompt — triage may or may not fire, doesn't matter
4333
+ await dispatchCodexNativeHook({
4334
+ hook_event_name: "UserPromptSubmit",
4335
+ cwd,
4336
+ session_id: sessionId,
4337
+ thread_id: "thread-kw-followup-1",
4338
+ turn_id: "turn-kw-followup-1",
4339
+ prompt: "hello",
4340
+ }, { cwd });
4341
+ // Turn 2: keyword prompt — keyword fast-path runs, triage does NOT add extra advisory
4342
+ const turn2 = await dispatchCodexNativeHook({
4343
+ hook_event_name: "UserPromptSubmit",
4344
+ cwd,
4345
+ session_id: sessionId,
4346
+ thread_id: "thread-kw-followup-1",
4347
+ turn_id: "turn-kw-followup-2",
4348
+ prompt: "$ralph continue",
4349
+ }, { cwd });
4350
+ assert.equal(turn2.skillState?.skill, "ralph");
4351
+ const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4352
+ assert.doesNotMatch(ctx2, /multi-step goal with no workflow keyword/);
4353
+ assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
4354
+ assert.doesNotMatch(ctx2, /narrow edit-shaped/);
4355
+ assert.doesNotMatch(ctx2, /visual\/style request/);
4356
+ // No triage state written on the keyword turn
4357
+ const triageState = join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json");
4358
+ // The state from turn 1 (if any) must not have been created either (hello = PASS)
4359
+ assert.equal(existsSync(triageState), false);
4360
+ }
4361
+ finally {
4362
+ await rm(cwd, { recursive: true, force: true });
4363
+ }
4364
+ });
4365
+ // ── Group 11: Config-disabled path ───────────────────────────────────────
4366
+ it("produces no triage advisory and no state when triage is disabled in config", async () => {
4367
+ const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-home-"));
4368
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-cwd-"));
4369
+ try {
4370
+ // Write a .omx-config.json in the fake CODEX_HOME that disables triage
4371
+ await writeJson(join(tmpHome, ".omx-config.json"), {
4372
+ promptRouting: { triage: { enabled: false } },
4373
+ });
4374
+ process.env.CODEX_HOME = tmpHome;
4375
+ resetTriageConfigCache();
4376
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4377
+ const result = await dispatchCodexNativeHook({
4378
+ hook_event_name: "UserPromptSubmit",
4379
+ cwd,
4380
+ session_id: "triage-disabled-1",
4381
+ thread_id: "thread-triage-disabled-1",
4382
+ turn_id: "turn-triage-disabled-1",
4383
+ prompt: "add dark mode toggle to the settings page",
4384
+ }, { cwd });
4385
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4386
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
4387
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-disabled-1", "prompt-routing-state.json");
4388
+ assert.equal(existsSync(stateFile), false);
4389
+ }
4390
+ finally {
4391
+ await rm(tmpHome, { recursive: true, force: true });
4392
+ await rm(cwd, { recursive: true, force: true });
4393
+ }
4394
+ });
4395
+ it("keeps triage default-enabled when config omits promptRouting.triage.enabled", async () => {
4396
+ const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-home-"));
4397
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-cwd-"));
4398
+ const previousCodexHome = process.env.CODEX_HOME;
4399
+ try {
4400
+ await writeJson(join(tmpHome, ".omx-config.json"), {
4401
+ promptRouting: {},
4402
+ });
4403
+ process.env.CODEX_HOME = tmpHome;
4404
+ resetTriageConfigCache();
4405
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4406
+ const result = await dispatchCodexNativeHook({
4407
+ hook_event_name: "UserPromptSubmit",
4408
+ cwd,
4409
+ session_id: "triage-defaulted-1",
4410
+ thread_id: "thread-triage-defaulted-1",
4411
+ turn_id: "turn-triage-defaulted-1",
4412
+ prompt: "add dark mode toggle to the settings page",
4413
+ }, { cwd });
4414
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4415
+ assert.match(additionalContext, /multi-step goal with no workflow keyword/);
4416
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-defaulted-1", "prompt-routing-state.json");
4417
+ assert.equal(existsSync(stateFile), true);
4418
+ }
4419
+ finally {
4420
+ if (typeof previousCodexHome === "string")
4421
+ process.env.CODEX_HOME = previousCodexHome;
4422
+ else
4423
+ delete process.env.CODEX_HOME;
4424
+ resetTriageConfigCache();
4425
+ await rm(tmpHome, { recursive: true, force: true });
4426
+ await rm(cwd, { recursive: true, force: true });
4427
+ }
4428
+ });
4429
+ it("does not suppress a short anchored follow-up that is a new request", async () => {
4430
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-short-new-request-"));
4431
+ const sessionId = "triage-short-new-request-1";
4432
+ try {
4433
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4434
+ await dispatchCodexNativeHook({
4435
+ hook_event_name: "UserPromptSubmit",
4436
+ cwd,
4437
+ session_id: sessionId,
4438
+ thread_id: "thread-short-new-request-1",
4439
+ turn_id: "turn-short-new-request-1",
4440
+ prompt: "add dark mode toggle to the settings page",
4441
+ }, { cwd });
4442
+ const turn2 = await dispatchCodexNativeHook({
4443
+ hook_event_name: "UserPromptSubmit",
4444
+ cwd,
4445
+ session_id: sessionId,
4446
+ thread_id: "thread-short-new-request-1",
4447
+ turn_id: "turn-short-new-request-2",
4448
+ prompt: "fix typo in src/foo.ts",
4449
+ }, { cwd });
4450
+ const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4451
+ assert.match(ctx2, /narrow edit-shaped/);
4452
+ }
4453
+ finally {
4454
+ await rm(cwd, { recursive: true, force: true });
4455
+ }
4456
+ });
4457
+ it("skips triage state persistence for malformed explicit session ids without writing root state", async () => {
4458
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-invalid-session-"));
4459
+ try {
4460
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
4461
+ const result = await dispatchCodexNativeHook({
4462
+ hook_event_name: "UserPromptSubmit",
4463
+ cwd,
4464
+ session_id: "bad/session",
4465
+ thread_id: "thread-triage-invalid-session-1",
4466
+ turn_id: "turn-triage-invalid-session-1",
4467
+ prompt: "add dark mode toggle to the settings page",
4468
+ }, { cwd });
4469
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
4470
+ assert.match(additionalContext, /multi-step goal with no workflow keyword/);
4471
+ assert.equal(existsSync(join(cwd, ".omx", "state", "prompt-routing-state.json")), false);
4472
+ }
4473
+ finally {
4474
+ await rm(cwd, { recursive: true, force: true });
4475
+ }
4476
+ });
4477
+ });
3145
4478
  //# sourceMappingURL=codex-native-hook.test.js.map