oh-my-codex 0.14.4 → 0.15.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 (614) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/Cargo.lock +6 -6
  3. package/Cargo.toml +2 -1
  4. package/README.md +40 -7
  5. package/crates/omx-explore/Cargo.toml +1 -0
  6. package/crates/omx-explore/src/main.rs +246 -24
  7. package/crates/omx-mux/Cargo.toml +1 -0
  8. package/crates/omx-runtime/Cargo.toml +1 -0
  9. package/crates/omx-runtime-core/Cargo.toml +1 -0
  10. package/crates/omx-runtime-core/src/dispatch.rs +2 -1
  11. package/crates/omx-runtime-core/src/engine.rs +2 -2
  12. package/crates/omx-runtime-core/src/mailbox.rs +2 -1
  13. package/crates/omx-sparkshell/Cargo.toml +1 -0
  14. package/crates/omx-sparkshell/src/codex_bridge.rs +1 -0
  15. package/crates/omx-sparkshell/src/prompt.rs +1 -0
  16. package/crates/omx-sparkshell/src/threshold.rs +1 -0
  17. package/dist/agents/__tests__/native-config.test.js +98 -4
  18. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  19. package/dist/agents/native-config.d.ts +4 -0
  20. package/dist/agents/native-config.d.ts.map +1 -1
  21. package/dist/agents/native-config.js +19 -3
  22. package/dist/agents/native-config.js.map +1 -1
  23. package/dist/agents/policy.d.ts +10 -0
  24. package/dist/agents/policy.d.ts.map +1 -0
  25. package/dist/agents/policy.js +61 -0
  26. package/dist/agents/policy.js.map +1 -0
  27. package/dist/catalog/__tests__/generator.test.js +2 -0
  28. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  29. package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts +2 -0
  30. package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts.map +1 -0
  31. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +72 -0
  32. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -0
  33. package/dist/catalog/installable.d.ts +5 -0
  34. package/dist/catalog/installable.d.ts.map +1 -0
  35. package/dist/catalog/installable.js +13 -0
  36. package/dist/catalog/installable.js.map +1 -0
  37. package/dist/catalog/skill-mirror.d.ts +20 -0
  38. package/dist/catalog/skill-mirror.d.ts.map +1 -0
  39. package/dist/catalog/skill-mirror.js +104 -0
  40. package/dist/catalog/skill-mirror.js.map +1 -0
  41. package/dist/cli/__tests__/ask.test.js +4 -1
  42. package/dist/cli/__tests__/ask.test.js.map +1 -1
  43. package/dist/cli/__tests__/codex-plugin-layout.test.d.ts +2 -0
  44. package/dist/cli/__tests__/codex-plugin-layout.test.d.ts.map +1 -0
  45. package/dist/cli/__tests__/codex-plugin-layout.test.js +210 -0
  46. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -0
  47. package/dist/cli/__tests__/doctor-team.test.js +46 -1
  48. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  49. package/dist/cli/__tests__/doctor-warning-copy.test.js +225 -111
  50. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  51. package/dist/cli/__tests__/exec.test.js +96 -1
  52. package/dist/cli/__tests__/exec.test.js.map +1 -1
  53. package/dist/cli/__tests__/explore.test.js +26 -9
  54. package/dist/cli/__tests__/explore.test.js.map +1 -1
  55. package/dist/cli/__tests__/index.test.js +301 -7
  56. package/dist/cli/__tests__/index.test.js.map +1 -1
  57. package/dist/cli/__tests__/launch-fallback.test.js +358 -4
  58. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  59. package/dist/cli/__tests__/list.test.d.ts +2 -0
  60. package/dist/cli/__tests__/list.test.d.ts.map +1 -0
  61. package/dist/cli/__tests__/list.test.js +38 -0
  62. package/dist/cli/__tests__/list.test.js.map +1 -0
  63. package/dist/cli/__tests__/mcp-parity.test.js +86 -0
  64. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  65. package/dist/cli/__tests__/mcp-serve.test.d.ts +2 -0
  66. package/dist/cli/__tests__/mcp-serve.test.d.ts.map +1 -0
  67. package/dist/cli/__tests__/mcp-serve.test.js +38 -0
  68. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -0
  69. package/dist/cli/__tests__/nested-help-routing.test.js +1 -0
  70. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  71. package/dist/cli/__tests__/package-bin-contract.test.js +58 -3
  72. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  73. package/dist/cli/__tests__/question.test.js +209 -5
  74. package/dist/cli/__tests__/question.test.js.map +1 -1
  75. package/dist/cli/__tests__/setup-agents-overwrite.test.js +146 -3
  76. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  77. package/dist/cli/__tests__/setup-install-mode.test.d.ts +2 -0
  78. package/dist/cli/__tests__/setup-install-mode.test.d.ts.map +1 -0
  79. package/dist/cli/__tests__/setup-install-mode.test.js +873 -0
  80. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -0
  81. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +83 -22
  82. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  83. package/dist/cli/__tests__/setup-refresh.test.js +120 -2
  84. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  85. package/dist/cli/__tests__/setup-skills-overwrite.test.js +32 -13
  86. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  87. package/dist/cli/__tests__/sidecar.test.d.ts +2 -0
  88. package/dist/cli/__tests__/sidecar.test.d.ts.map +1 -0
  89. package/dist/cli/__tests__/sidecar.test.js +24 -0
  90. package/dist/cli/__tests__/sidecar.test.js.map +1 -0
  91. package/dist/cli/__tests__/team.test.js +90 -42
  92. package/dist/cli/__tests__/team.test.js.map +1 -1
  93. package/dist/cli/__tests__/version-sync-contract.test.js +11 -0
  94. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  95. package/dist/cli/codex-home.d.ts +4 -5
  96. package/dist/cli/codex-home.d.ts.map +1 -1
  97. package/dist/cli/codex-home.js +9 -37
  98. package/dist/cli/codex-home.js.map +1 -1
  99. package/dist/cli/doctor.d.ts +1 -1
  100. package/dist/cli/doctor.d.ts.map +1 -1
  101. package/dist/cli/doctor.js +509 -278
  102. package/dist/cli/doctor.js.map +1 -1
  103. package/dist/cli/index.d.ts +13 -5
  104. package/dist/cli/index.d.ts.map +1 -1
  105. package/dist/cli/index.js +351 -60
  106. package/dist/cli/index.js.map +1 -1
  107. package/dist/cli/list.d.ts +2 -0
  108. package/dist/cli/list.d.ts.map +1 -0
  109. package/dist/cli/list.js +40 -0
  110. package/dist/cli/list.js.map +1 -0
  111. package/dist/cli/mcp-serve.d.ts +12 -0
  112. package/dist/cli/mcp-serve.d.ts.map +1 -0
  113. package/dist/cli/mcp-serve.js +63 -0
  114. package/dist/cli/mcp-serve.js.map +1 -0
  115. package/dist/cli/plugin-marketplace.d.ts +13 -0
  116. package/dist/cli/plugin-marketplace.d.ts.map +1 -0
  117. package/dist/cli/plugin-marketplace.js +77 -0
  118. package/dist/cli/plugin-marketplace.js.map +1 -0
  119. package/dist/cli/question.d.ts.map +1 -1
  120. package/dist/cli/question.js +27 -2
  121. package/dist/cli/question.js.map +1 -1
  122. package/dist/cli/setup-preferences.d.ts +20 -0
  123. package/dist/cli/setup-preferences.d.ts.map +1 -0
  124. package/dist/cli/setup-preferences.js +71 -0
  125. package/dist/cli/setup-preferences.js.map +1 -0
  126. package/dist/cli/setup.d.ts +12 -3
  127. package/dist/cli/setup.d.ts.map +1 -1
  128. package/dist/cli/setup.js +1080 -254
  129. package/dist/cli/setup.js.map +1 -1
  130. package/dist/cli/team.d.ts +1 -0
  131. package/dist/cli/team.d.ts.map +1 -1
  132. package/dist/cli/team.js +22 -3
  133. package/dist/cli/team.js.map +1 -1
  134. package/dist/cli/uninstall.d.ts.map +1 -1
  135. package/dist/cli/uninstall.js +2 -8
  136. package/dist/cli/uninstall.js.map +1 -1
  137. package/dist/config/__tests__/generator-idempotent.test.js +100 -3
  138. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  139. package/dist/config/__tests__/generator-notify.test.js +6 -5
  140. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  141. package/dist/config/__tests__/generator-status-line-presets.test.d.ts +2 -0
  142. package/dist/config/__tests__/generator-status-line-presets.test.d.ts.map +1 -0
  143. package/dist/config/__tests__/generator-status-line-presets.test.js +203 -0
  144. package/dist/config/__tests__/generator-status-line-presets.test.js.map +1 -0
  145. package/dist/config/__tests__/models.test.js +35 -2
  146. package/dist/config/__tests__/models.test.js.map +1 -1
  147. package/dist/config/codex-hooks.d.ts.map +1 -1
  148. package/dist/config/codex-hooks.js +2 -7
  149. package/dist/config/codex-hooks.js.map +1 -1
  150. package/dist/config/generator.d.ts +10 -0
  151. package/dist/config/generator.d.ts.map +1 -1
  152. package/dist/config/generator.js +259 -76
  153. package/dist/config/generator.js.map +1 -1
  154. package/dist/config/models.d.ts +13 -3
  155. package/dist/config/models.d.ts.map +1 -1
  156. package/dist/config/models.js +25 -5
  157. package/dist/config/models.js.map +1 -1
  158. package/dist/config/omx-first-party-mcp.d.ts +18 -0
  159. package/dist/config/omx-first-party-mcp.d.ts.map +1 -0
  160. package/dist/config/omx-first-party-mcp.js +76 -0
  161. package/dist/config/omx-first-party-mcp.js.map +1 -0
  162. package/dist/document-refresh/__tests__/enforcer.test.d.ts +2 -0
  163. package/dist/document-refresh/__tests__/enforcer.test.d.ts.map +1 -0
  164. package/dist/document-refresh/__tests__/enforcer.test.js +128 -0
  165. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -0
  166. package/dist/document-refresh/config.d.ts +9 -0
  167. package/dist/document-refresh/config.d.ts.map +1 -0
  168. package/dist/document-refresh/config.js +70 -0
  169. package/dist/document-refresh/config.js.map +1 -0
  170. package/dist/document-refresh/enforcer.d.ts +43 -0
  171. package/dist/document-refresh/enforcer.d.ts.map +1 -0
  172. package/dist/document-refresh/enforcer.js +329 -0
  173. package/dist/document-refresh/enforcer.js.map +1 -0
  174. package/dist/exec/followup.d.ts +44 -0
  175. package/dist/exec/followup.d.ts.map +1 -0
  176. package/dist/exec/followup.js +349 -0
  177. package/dist/exec/followup.js.map +1 -0
  178. package/dist/hooks/__tests__/codebase-map.test.js +63 -1
  179. package/dist/hooks/__tests__/codebase-map.test.js.map +1 -1
  180. package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -7
  181. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  182. package/dist/hooks/__tests__/keyword-detector.test.js +89 -0
  183. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  184. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +84 -1
  185. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  186. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +23 -2
  187. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  188. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +43 -0
  189. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  190. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +17 -0
  191. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  192. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +53 -0
  193. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  194. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +4 -4
  195. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  196. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +103 -0
  197. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  198. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +27 -13
  199. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  200. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts +2 -0
  201. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts.map +1 -0
  202. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +35 -0
  203. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -0
  204. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +70 -3
  205. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  206. package/dist/hooks/__tests__/pre-context-gate-skills.test.js +5 -0
  207. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  208. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +3 -2
  209. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  210. package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts +2 -0
  211. package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts.map +1 -0
  212. package/dist/hooks/__tests__/prompt-refactor-contract.test.js +22 -0
  213. package/dist/hooks/__tests__/prompt-refactor-contract.test.js.map +1 -0
  214. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts +2 -0
  215. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts.map +1 -0
  216. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +28 -0
  217. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -0
  218. package/dist/hooks/__tests__/triage-heuristic.test.js +59 -4
  219. package/dist/hooks/__tests__/triage-heuristic.test.js.map +1 -1
  220. package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts +2 -0
  221. package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts.map +1 -0
  222. package/dist/hooks/__tests__/visual-ralph-skill.test.js +44 -0
  223. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -0
  224. package/dist/hooks/codebase-map.d.ts.map +1 -1
  225. package/dist/hooks/codebase-map.js +83 -6
  226. package/dist/hooks/codebase-map.js.map +1 -1
  227. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  228. package/dist/hooks/keyword-detector.js +19 -14
  229. package/dist/hooks/keyword-detector.js.map +1 -1
  230. package/dist/hooks/prompt-guidance-contract.d.ts +6 -0
  231. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  232. package/dist/hooks/prompt-guidance-contract.js +89 -0
  233. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  234. package/dist/hooks/session.d.ts +2 -0
  235. package/dist/hooks/session.d.ts.map +1 -1
  236. package/dist/hooks/session.js +6 -0
  237. package/dist/hooks/session.js.map +1 -1
  238. package/dist/hooks/triage-heuristic.d.ts +2 -2
  239. package/dist/hooks/triage-heuristic.d.ts.map +1 -1
  240. package/dist/hooks/triage-heuristic.js +97 -20
  241. package/dist/hooks/triage-heuristic.js.map +1 -1
  242. package/dist/hooks/triage-state.d.ts +1 -1
  243. package/dist/hooks/triage-state.d.ts.map +1 -1
  244. package/dist/hooks/triage-state.js +2 -1
  245. package/dist/hooks/triage-state.js.map +1 -1
  246. package/dist/hud/__tests__/index.test.js +4 -4
  247. package/dist/hud/__tests__/index.test.js.map +1 -1
  248. package/dist/hud/__tests__/state.test.js +4 -0
  249. package/dist/hud/__tests__/state.test.js.map +1 -1
  250. package/dist/hud/__tests__/types.test.js +27 -0
  251. package/dist/hud/__tests__/types.test.js.map +1 -1
  252. package/dist/hud/state.d.ts.map +1 -1
  253. package/dist/hud/state.js +8 -0
  254. package/dist/hud/state.js.map +1 -1
  255. package/dist/hud/types.d.ts +9 -0
  256. package/dist/hud/types.d.ts.map +1 -1
  257. package/dist/hud/types.js +3 -0
  258. package/dist/hud/types.js.map +1 -1
  259. package/dist/mcp/__tests__/bootstrap.test.js +45 -5
  260. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  261. package/dist/mcp/__tests__/server-lifecycle.test.js +50 -7
  262. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  263. package/dist/mcp/__tests__/state-server.test.js +67 -9
  264. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  265. package/dist/mcp/bootstrap.d.ts +12 -1
  266. package/dist/mcp/bootstrap.d.ts.map +1 -1
  267. package/dist/mcp/bootstrap.js +87 -29
  268. package/dist/mcp/bootstrap.js.map +1 -1
  269. package/dist/mcp/state-server.d.ts +5 -11
  270. package/dist/mcp/state-server.d.ts.map +1 -1
  271. package/dist/mcp/state-server.js +16 -432
  272. package/dist/mcp/state-server.js.map +1 -1
  273. package/dist/planning/__tests__/artifacts.test.js +152 -1
  274. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  275. package/dist/planning/artifacts.d.ts +9 -0
  276. package/dist/planning/artifacts.d.ts.map +1 -1
  277. package/dist/planning/artifacts.js +60 -1
  278. package/dist/planning/artifacts.js.map +1 -1
  279. package/dist/question/__tests__/deep-interview.test.js +1 -1
  280. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  281. package/dist/question/__tests__/renderer.test.js +70 -0
  282. package/dist/question/__tests__/renderer.test.js.map +1 -1
  283. package/dist/question/__tests__/ui.test.js +49 -0
  284. package/dist/question/__tests__/ui.test.js.map +1 -1
  285. package/dist/question/renderer.d.ts +14 -1
  286. package/dist/question/renderer.d.ts.map +1 -1
  287. package/dist/question/renderer.js +109 -10
  288. package/dist/question/renderer.js.map +1 -1
  289. package/dist/question/state.d.ts +2 -0
  290. package/dist/question/state.d.ts.map +1 -1
  291. package/dist/question/state.js +4 -0
  292. package/dist/question/state.js.map +1 -1
  293. package/dist/question/types.d.ts +2 -1
  294. package/dist/question/types.d.ts.map +1 -1
  295. package/dist/question/types.js.map +1 -1
  296. package/dist/question/ui.d.ts.map +1 -1
  297. package/dist/question/ui.js +28 -8
  298. package/dist/question/ui.js.map +1 -1
  299. package/dist/runtime/__tests__/bridge.test.js +143 -1
  300. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  301. package/dist/runtime/bridge.d.ts +21 -0
  302. package/dist/runtime/bridge.d.ts.map +1 -1
  303. package/dist/runtime/bridge.js +54 -4
  304. package/dist/runtime/bridge.js.map +1 -1
  305. package/dist/scripts/__tests__/codex-native-hook.test.js +1904 -230
  306. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  307. package/dist/scripts/__tests__/generate-release-body.test.js +90 -1
  308. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  309. package/dist/scripts/__tests__/hook-derived-watcher.test.js +110 -2
  310. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  311. package/dist/scripts/__tests__/postinstall.test.js +7 -93
  312. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  313. package/dist/scripts/__tests__/prompt-inventory.test.d.ts +2 -0
  314. package/dist/scripts/__tests__/prompt-inventory.test.d.ts.map +1 -0
  315. package/dist/scripts/__tests__/prompt-inventory.test.js +56 -0
  316. package/dist/scripts/__tests__/prompt-inventory.test.js.map +1 -0
  317. package/dist/scripts/__tests__/run-test-files.test.d.ts +2 -0
  318. package/dist/scripts/__tests__/run-test-files.test.d.ts.map +1 -0
  319. package/dist/scripts/__tests__/run-test-files.test.js +62 -0
  320. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -0
  321. package/dist/scripts/__tests__/smoke-packed-install.test.js +13 -1
  322. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  323. package/dist/scripts/__tests__/verify-native-agents.test.d.ts +2 -0
  324. package/dist/scripts/__tests__/verify-native-agents.test.d.ts.map +1 -0
  325. package/dist/scripts/__tests__/verify-native-agents.test.js +166 -0
  326. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -0
  327. package/dist/scripts/codex-execution-surface.d.ts +16 -0
  328. package/dist/scripts/codex-execution-surface.d.ts.map +1 -0
  329. package/dist/scripts/codex-execution-surface.js +42 -0
  330. package/dist/scripts/codex-execution-surface.js.map +1 -0
  331. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  332. package/dist/scripts/codex-native-hook.js +418 -76
  333. package/dist/scripts/codex-native-hook.js.map +1 -1
  334. package/dist/scripts/codex-native-pre-post.d.ts +8 -0
  335. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  336. package/dist/scripts/codex-native-pre-post.js +133 -19
  337. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  338. package/dist/scripts/generate-release-body.d.ts +1 -0
  339. package/dist/scripts/generate-release-body.d.ts.map +1 -1
  340. package/dist/scripts/generate-release-body.js +32 -3
  341. package/dist/scripts/generate-release-body.js.map +1 -1
  342. package/dist/scripts/hook-derived-watcher.js +46 -7
  343. package/dist/scripts/hook-derived-watcher.js.map +1 -1
  344. package/dist/scripts/notify-fallback-watcher.js +59 -9
  345. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  346. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts +2 -0
  347. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts.map +1 -0
  348. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js +153 -0
  349. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js.map +1 -0
  350. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  351. package/dist/scripts/notify-hook/managed-tmux.js +18 -1
  352. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  353. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  354. package/dist/scripts/notify-hook/process-runner.js +7 -3
  355. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  356. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  357. package/dist/scripts/notify-hook/team-dispatch.js +96 -11
  358. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  359. package/dist/scripts/notify-hook/team-tmux-guard.js +3 -3
  360. package/dist/scripts/notify-hook/team-worker-posttooluse.d.ts +34 -0
  361. package/dist/scripts/notify-hook/team-worker-posttooluse.d.ts.map +1 -0
  362. package/dist/scripts/notify-hook/team-worker-posttooluse.js +434 -0
  363. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -0
  364. package/dist/scripts/notify-hook/team-worker.d.ts +1 -1
  365. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  366. package/dist/scripts/notify-hook/team-worker.js +3 -43
  367. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  368. package/dist/scripts/notify-hook.js +36 -5
  369. package/dist/scripts/notify-hook.js.map +1 -1
  370. package/dist/scripts/postinstall.d.ts +1 -5
  371. package/dist/scripts/postinstall.d.ts.map +1 -1
  372. package/dist/scripts/postinstall.js +3 -42
  373. package/dist/scripts/postinstall.js.map +1 -1
  374. package/dist/scripts/prompt-inventory.d.ts +29 -0
  375. package/dist/scripts/prompt-inventory.d.ts.map +1 -0
  376. package/dist/scripts/prompt-inventory.js +178 -0
  377. package/dist/scripts/prompt-inventory.js.map +1 -0
  378. package/dist/scripts/run-test-files.js +32 -2
  379. package/dist/scripts/run-test-files.js.map +1 -1
  380. package/dist/scripts/smoke-packed-install.d.ts +3 -0
  381. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  382. package/dist/scripts/smoke-packed-install.js +9 -1
  383. package/dist/scripts/smoke-packed-install.js.map +1 -1
  384. package/dist/scripts/sync-plugin-mirror.d.ts +13 -0
  385. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -0
  386. package/dist/scripts/sync-plugin-mirror.js +242 -0
  387. package/dist/scripts/sync-plugin-mirror.js.map +1 -0
  388. package/dist/scripts/verify-native-agents.d.ts +16 -0
  389. package/dist/scripts/verify-native-agents.d.ts.map +1 -0
  390. package/dist/scripts/verify-native-agents.js +188 -0
  391. package/dist/scripts/verify-native-agents.js.map +1 -0
  392. package/dist/session-history/search.d.ts.map +1 -1
  393. package/dist/session-history/search.js +7 -2
  394. package/dist/session-history/search.js.map +1 -1
  395. package/dist/sidecar/__tests__/boundary.test.d.ts +2 -0
  396. package/dist/sidecar/__tests__/boundary.test.d.ts.map +1 -0
  397. package/dist/sidecar/__tests__/boundary.test.js +48 -0
  398. package/dist/sidecar/__tests__/boundary.test.js.map +1 -0
  399. package/dist/sidecar/__tests__/collector.test.d.ts +2 -0
  400. package/dist/sidecar/__tests__/collector.test.d.ts.map +1 -0
  401. package/dist/sidecar/__tests__/collector.test.js +162 -0
  402. package/dist/sidecar/__tests__/collector.test.js.map +1 -0
  403. package/dist/sidecar/__tests__/render.test.d.ts +2 -0
  404. package/dist/sidecar/__tests__/render.test.d.ts.map +1 -0
  405. package/dist/sidecar/__tests__/render.test.js +67 -0
  406. package/dist/sidecar/__tests__/render.test.js.map +1 -0
  407. package/dist/sidecar/__tests__/tmux.test.d.ts +2 -0
  408. package/dist/sidecar/__tests__/tmux.test.d.ts.map +1 -0
  409. package/dist/sidecar/__tests__/tmux.test.js +30 -0
  410. package/dist/sidecar/__tests__/tmux.test.js.map +1 -0
  411. package/dist/sidecar/__tests__/watch.test.d.ts +2 -0
  412. package/dist/sidecar/__tests__/watch.test.d.ts.map +1 -0
  413. package/dist/sidecar/__tests__/watch.test.js +42 -0
  414. package/dist/sidecar/__tests__/watch.test.js.map +1 -0
  415. package/dist/sidecar/collector.d.ts +4 -0
  416. package/dist/sidecar/collector.d.ts.map +1 -0
  417. package/dist/sidecar/collector.js +377 -0
  418. package/dist/sidecar/collector.js.map +1 -0
  419. package/dist/sidecar/index.d.ts +25 -0
  420. package/dist/sidecar/index.d.ts.map +1 -0
  421. package/dist/sidecar/index.js +165 -0
  422. package/dist/sidecar/index.js.map +1 -0
  423. package/dist/sidecar/render.d.ts +3 -0
  424. package/dist/sidecar/render.d.ts.map +1 -0
  425. package/dist/sidecar/render.js +72 -0
  426. package/dist/sidecar/render.js.map +1 -0
  427. package/dist/sidecar/tmux.d.ts +13 -0
  428. package/dist/sidecar/tmux.d.ts.map +1 -0
  429. package/dist/sidecar/tmux.js +44 -0
  430. package/dist/sidecar/tmux.js.map +1 -0
  431. package/dist/sidecar/types.d.ts +125 -0
  432. package/dist/sidecar/types.d.ts.map +1 -0
  433. package/dist/sidecar/types.js +2 -0
  434. package/dist/sidecar/types.js.map +1 -0
  435. package/dist/state/__tests__/operations.test.js +50 -22
  436. package/dist/state/__tests__/operations.test.js.map +1 -1
  437. package/dist/state/operations.d.ts +1 -1
  438. package/dist/state/operations.d.ts.map +1 -1
  439. package/dist/state/operations.js +19 -7
  440. package/dist/state/operations.js.map +1 -1
  441. package/dist/team/__tests__/commit-hygiene.test.d.ts +2 -0
  442. package/dist/team/__tests__/commit-hygiene.test.d.ts.map +1 -0
  443. package/dist/team/__tests__/commit-hygiene.test.js +93 -0
  444. package/dist/team/__tests__/commit-hygiene.test.js.map +1 -0
  445. package/dist/team/__tests__/delegation-policy.test.d.ts +2 -0
  446. package/dist/team/__tests__/delegation-policy.test.d.ts.map +1 -0
  447. package/dist/team/__tests__/delegation-policy.test.js +69 -0
  448. package/dist/team/__tests__/delegation-policy.test.js.map +1 -0
  449. package/dist/team/__tests__/events.test.js +54 -4
  450. package/dist/team/__tests__/events.test.js.map +1 -1
  451. package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts +2 -0
  452. package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts.map +1 -0
  453. package/dist/team/__tests__/hook-primary-e2e-contract.test.js +78 -0
  454. package/dist/team/__tests__/hook-primary-e2e-contract.test.js.map +1 -0
  455. package/dist/team/__tests__/model-contract.test.js +18 -2
  456. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  457. package/dist/team/__tests__/repo-aware-decomposition.test.d.ts +2 -0
  458. package/dist/team/__tests__/repo-aware-decomposition.test.d.ts.map +1 -0
  459. package/dist/team/__tests__/repo-aware-decomposition.test.js +95 -0
  460. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -0
  461. package/dist/team/__tests__/runtime.test.js +677 -17
  462. package/dist/team/__tests__/runtime.test.js.map +1 -1
  463. package/dist/team/__tests__/state-root.test.js +177 -1
  464. package/dist/team/__tests__/state-root.test.js.map +1 -1
  465. package/dist/team/__tests__/state.test.js +110 -0
  466. package/dist/team/__tests__/state.test.js.map +1 -1
  467. package/dist/team/__tests__/tmux-session.test.js +301 -2
  468. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  469. package/dist/team/__tests__/worker-bootstrap.test.js +94 -0
  470. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  471. package/dist/team/commit-hygiene.d.ts +22 -3
  472. package/dist/team/commit-hygiene.d.ts.map +1 -1
  473. package/dist/team/commit-hygiene.js +134 -2
  474. package/dist/team/commit-hygiene.js.map +1 -1
  475. package/dist/team/contracts.d.ts +1 -1
  476. package/dist/team/contracts.d.ts.map +1 -1
  477. package/dist/team/contracts.js +2 -0
  478. package/dist/team/contracts.js.map +1 -1
  479. package/dist/team/dag-schema.d.ts +38 -0
  480. package/dist/team/dag-schema.d.ts.map +1 -0
  481. package/dist/team/dag-schema.js +221 -0
  482. package/dist/team/dag-schema.js.map +1 -0
  483. package/dist/team/delegation-policy.d.ts +3 -0
  484. package/dist/team/delegation-policy.d.ts.map +1 -0
  485. package/dist/team/delegation-policy.js +82 -0
  486. package/dist/team/delegation-policy.js.map +1 -0
  487. package/dist/team/model-contract.d.ts +3 -1
  488. package/dist/team/model-contract.d.ts.map +1 -1
  489. package/dist/team/model-contract.js +44 -5
  490. package/dist/team/model-contract.js.map +1 -1
  491. package/dist/team/repo-aware-decomposition.d.ts +60 -0
  492. package/dist/team/repo-aware-decomposition.d.ts.map +1 -0
  493. package/dist/team/repo-aware-decomposition.js +229 -0
  494. package/dist/team/repo-aware-decomposition.js.map +1 -0
  495. package/dist/team/runtime.d.ts +34 -0
  496. package/dist/team/runtime.d.ts.map +1 -1
  497. package/dist/team/runtime.js +191 -50
  498. package/dist/team/runtime.js.map +1 -1
  499. package/dist/team/state/tasks.d.ts.map +1 -1
  500. package/dist/team/state/tasks.js +33 -0
  501. package/dist/team/state/tasks.js.map +1 -1
  502. package/dist/team/state/types.d.ts +23 -1
  503. package/dist/team/state/types.d.ts.map +1 -1
  504. package/dist/team/state/types.js.map +1 -1
  505. package/dist/team/state-root.d.ts +35 -0
  506. package/dist/team/state-root.d.ts.map +1 -1
  507. package/dist/team/state-root.js +281 -1
  508. package/dist/team/state-root.js.map +1 -1
  509. package/dist/team/state.d.ts +27 -1
  510. package/dist/team/state.d.ts.map +1 -1
  511. package/dist/team/state.js +6 -0
  512. package/dist/team/state.js.map +1 -1
  513. package/dist/team/tmux-session.d.ts +1 -0
  514. package/dist/team/tmux-session.d.ts.map +1 -1
  515. package/dist/team/tmux-session.js +83 -6
  516. package/dist/team/tmux-session.js.map +1 -1
  517. package/dist/team/worker-bootstrap.d.ts +3 -0
  518. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  519. package/dist/team/worker-bootstrap.js +77 -4
  520. package/dist/team/worker-bootstrap.js.map +1 -1
  521. package/dist/utils/__tests__/agents-model-table.test.js +8 -0
  522. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  523. package/dist/utils/__tests__/paths.test.js +42 -9
  524. package/dist/utils/__tests__/paths.test.js.map +1 -1
  525. package/dist/utils/agents-md.d.ts +3 -0
  526. package/dist/utils/agents-md.d.ts.map +1 -1
  527. package/dist/utils/agents-md.js +25 -0
  528. package/dist/utils/agents-md.js.map +1 -1
  529. package/dist/utils/agents-model-table.d.ts.map +1 -1
  530. package/dist/utils/agents-model-table.js +2 -3
  531. package/dist/utils/agents-model-table.js.map +1 -1
  532. package/package.json +14 -7
  533. package/plugins/oh-my-codex/.app.json +3 -0
  534. package/plugins/oh-my-codex/.codex-plugin/plugin.json +30 -0
  535. package/plugins/oh-my-codex/.mcp.json +44 -0
  536. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +114 -0
  537. package/plugins/oh-my-codex/skills/analyze/SKILL.md +148 -0
  538. package/plugins/oh-my-codex/skills/ask-claude/SKILL.md +61 -0
  539. package/plugins/oh-my-codex/skills/ask-gemini/SKILL.md +61 -0
  540. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +231 -0
  541. package/plugins/oh-my-codex/skills/autoresearch/SKILL.md +68 -0
  542. package/plugins/oh-my-codex/skills/cancel/SKILL.md +399 -0
  543. package/plugins/oh-my-codex/skills/code-review/SKILL.md +290 -0
  544. package/plugins/oh-my-codex/skills/configure-notifications/SKILL.md +287 -0
  545. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +464 -0
  546. package/plugins/oh-my-codex/skills/doctor/SKILL.md +239 -0
  547. package/plugins/oh-my-codex/skills/help/SKILL.md +202 -0
  548. package/plugins/oh-my-codex/skills/hud/SKILL.md +98 -0
  549. package/plugins/oh-my-codex/skills/note/SKILL.md +62 -0
  550. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +135 -0
  551. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +86 -0
  552. package/plugins/oh-my-codex/skills/plan/SKILL.md +276 -0
  553. package/plugins/oh-my-codex/skills/ralph/SKILL.md +269 -0
  554. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +162 -0
  555. package/plugins/oh-my-codex/skills/security-review/SKILL.md +300 -0
  556. package/plugins/oh-my-codex/skills/skill/SKILL.md +835 -0
  557. package/plugins/oh-my-codex/skills/team/SKILL.md +510 -0
  558. package/plugins/oh-my-codex/skills/trace/SKILL.md +33 -0
  559. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +143 -0
  560. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +175 -0
  561. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +153 -0
  562. package/plugins/oh-my-codex/skills/visual-verdict/SKILL.md +76 -0
  563. package/plugins/oh-my-codex/skills/wiki/SKILL.md +57 -0
  564. package/plugins/oh-my-codex/skills/worker/SKILL.md +106 -0
  565. package/prompts/critic.md +35 -83
  566. package/prompts/executor.md +32 -109
  567. package/prompts/explore-harness.md +1 -1
  568. package/prompts/explore.md +37 -90
  569. package/prompts/planner.md +28 -58
  570. package/prompts/researcher.md +46 -78
  571. package/prompts/verifier.md +23 -31
  572. package/skills/autopilot/SKILL.md +1 -4
  573. package/skills/deep-interview/SKILL.md +8 -5
  574. package/skills/doctor/SKILL.md +45 -17
  575. package/skills/help/SKILL.md +2 -2
  576. package/skills/omx-setup/SKILL.md +52 -9
  577. package/skills/plan/SKILL.md +1 -4
  578. package/skills/ralph/SKILL.md +3 -5
  579. package/skills/ralplan/SKILL.md +2 -6
  580. package/skills/team/SKILL.md +2 -5
  581. package/skills/ultraqa/SKILL.md +1 -4
  582. package/skills/ultrawork/SKILL.md +2 -3
  583. package/skills/visual-ralph/SKILL.md +153 -0
  584. package/skills/web-clone/SKILL.md +12 -354
  585. package/src/scripts/__tests__/codex-native-hook.test.ts +2306 -224
  586. package/src/scripts/__tests__/generate-release-body.test.ts +109 -0
  587. package/src/scripts/__tests__/hook-derived-watcher.test.ts +132 -2
  588. package/src/scripts/__tests__/postinstall.test.ts +7 -104
  589. package/src/scripts/__tests__/prompt-inventory.test.ts +64 -0
  590. package/src/scripts/__tests__/run-test-files.test.ts +72 -0
  591. package/src/scripts/__tests__/smoke-packed-install.test.ts +15 -0
  592. package/src/scripts/__tests__/verify-native-agents.test.ts +204 -0
  593. package/src/scripts/codex-execution-surface.ts +73 -0
  594. package/src/scripts/codex-native-hook.ts +579 -82
  595. package/src/scripts/codex-native-pre-post.ts +157 -19
  596. package/src/scripts/generate-release-body.ts +34 -2
  597. package/src/scripts/hook-derived-watcher.ts +51 -8
  598. package/src/scripts/notify-fallback-watcher.ts +65 -9
  599. package/src/scripts/notify-hook/__tests__/team-worker-posttooluse.test.ts +180 -0
  600. package/src/scripts/notify-hook/managed-tmux.ts +22 -4
  601. package/src/scripts/notify-hook/process-runner.ts +7 -3
  602. package/src/scripts/notify-hook/team-dispatch.ts +103 -11
  603. package/src/scripts/notify-hook/team-tmux-guard.ts +3 -3
  604. package/src/scripts/notify-hook/team-worker-posttooluse.ts +536 -0
  605. package/src/scripts/notify-hook/team-worker.ts +4 -48
  606. package/src/scripts/notify-hook.ts +36 -5
  607. package/src/scripts/postinstall.ts +4 -58
  608. package/src/scripts/prompt-inventory.ts +218 -0
  609. package/src/scripts/run-test-files.ts +41 -2
  610. package/src/scripts/smoke-packed-install.ts +10 -1
  611. package/src/scripts/sync-plugin-mirror.ts +360 -0
  612. package/src/scripts/verify-native-agents.ts +245 -0
  613. package/templates/AGENTS.md +26 -91
  614. package/templates/catalog-manifest.json +16 -1
@@ -1,20 +1,49 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
- import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { afterEach, beforeEach, describe, it } from "node:test";
9
9
  import { buildManagedCodexHooksConfig } from "../../config/codex-hooks.js";
10
+ import { DOCUMENT_REFRESH_EXEMPTION_PREFIX } from "../../document-refresh/enforcer.js";
10
11
  import { initTeamState, readTeamLeaderAttention, readTeamPhase, writeTeamLeaderAttention, } from "../../team/state.js";
11
12
  import { dispatchCodexNativeHook, isCodexNativeHookMainModule, mapCodexHookEventToOmxEvent, resolveSessionOwnerPidFromAncestry, } from "../codex-native-hook.js";
12
13
  import { writeSessionStart } from "../../hooks/session.js";
13
14
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
15
+ import { executeStateOperation } from "../../state/operations.js";
16
+ function nativeHookScriptPath() {
17
+ return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
18
+ }
19
+ function parseSingleJsonStdout(stdout) {
20
+ const trimmed = stdout.trim();
21
+ assert.notEqual(trimmed, "");
22
+ assert.equal(trimmed.split("\n").length, 1);
23
+ return JSON.parse(trimmed);
24
+ }
25
+ function runNativeHookCli(payload, options = {}) {
26
+ return execFileSync(process.execPath, [nativeHookScriptPath()], {
27
+ cwd: options.cwd ?? process.cwd(),
28
+ input: typeof payload === "string" ? payload : JSON.stringify(payload),
29
+ encoding: "utf-8",
30
+ stdio: ["pipe", "pipe", "pipe"],
31
+ env: options.env ?? process.env,
32
+ });
33
+ }
14
34
  async function writeJson(path, value) {
15
35
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
16
36
  await writeFile(path, JSON.stringify(value, null, 2));
17
37
  }
38
+ async function writeActiveAutopilotSession(cwd, sessionId) {
39
+ await writeJson(join(cwd, ".omx", "state", "session.json"), {
40
+ session_id: sessionId,
41
+ });
42
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
43
+ active: true,
44
+ current_phase: "execution",
45
+ });
46
+ }
18
47
  async function writeHookCounterPlugin(cwd) {
19
48
  const markerPath = join(cwd, ".omx", "stop-hook-counter.json");
20
49
  await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
@@ -67,6 +96,9 @@ const TEAM_ENV_KEYS = [
67
96
  "OMX_TEAM_STATE_ROOT",
68
97
  "OMX_TEAM_LEADER_CWD",
69
98
  "OMX_SESSION_ID",
99
+ "OMX_QUESTION_RETURN_PANE",
100
+ "OMX_LEADER_PANE_ID",
101
+ "TMUX",
70
102
  "TMUX_PANE",
71
103
  ];
72
104
  const priorTeamEnv = new Map();
@@ -103,10 +135,15 @@ describe("codex native hook config", () => {
103
135
  const preToolUse = config.hooks.PreToolUse[0];
104
136
  assert.equal(preToolUse.matcher, "Bash");
105
137
  assert.match(String(preToolUse.hooks?.[0]?.command || ""), /codex-native-hook\.js"?$/);
138
+ assert.equal(preToolUse.hooks?.[0]?.statusMessage, undefined);
106
139
  const postToolUse = config.hooks.PostToolUse[0];
107
140
  assert.equal(postToolUse.matcher, undefined);
108
141
  assert.match(String(postToolUse.hooks?.[0]?.command || ""), /codex-native-hook\.js"?$/);
109
- assert.equal(postToolUse.hooks?.[0]?.statusMessage, "Running OMX tool review");
142
+ assert.equal(postToolUse.hooks?.[0]?.statusMessage, undefined);
143
+ const userPromptSubmit = config.hooks.UserPromptSubmit[0];
144
+ assert.equal(userPromptSubmit.matcher, undefined);
145
+ assert.match(String(userPromptSubmit.hooks?.[0]?.command || ""), /codex-native-hook\.js"?$/);
146
+ assert.equal(userPromptSubmit.hooks?.[0]?.statusMessage, undefined);
110
147
  const stop = config.hooks.Stop[0];
111
148
  assert.equal(stop.hooks?.[0]?.timeout, 30);
112
149
  });
@@ -120,18 +157,124 @@ describe("codex native hook dispatch", () => {
120
157
  assert.equal(isCodexNativeHookMainModule(pathToFileURL("/tmp/omx native/other-script.js").href, "/tmp/omx native/codex-native-hook.js"), false);
121
158
  });
122
159
  it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
123
- const stdout = execFileSync(process.execPath, [join(process.cwd(), "dist", "scripts", "codex-native-hook.js")], {
124
- cwd: process.cwd(),
125
- input: "{",
126
- encoding: "utf-8",
127
- stdio: ["pipe", "pipe", "pipe"],
128
- });
129
- const output = JSON.parse(stdout.trim());
160
+ const stdout = runNativeHookCli("{");
161
+ const output = parseSingleJsonStdout(stdout);
130
162
  assert.equal(output.decision, "block");
131
163
  assert.equal(output.reason, "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.");
132
164
  assert.equal(output.hookSpecificOutput?.hookEventName, "Unknown");
133
165
  assert.match(String(output.hookSpecificOutput?.additionalContext ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
134
166
  });
167
+ it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
168
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
169
+ try {
170
+ const stdout = runNativeHookCli({
171
+ hook_event_name: "Stop",
172
+ cwd,
173
+ session_id: "sess-cli-stop-noop-json",
174
+ thread_id: "thread-cli-stop-noop-json",
175
+ turn_id: "turn-cli-stop-noop-json",
176
+ }, { cwd });
177
+ const output = parseSingleJsonStdout(stdout);
178
+ assert.deepEqual(output, {});
179
+ }
180
+ finally {
181
+ await rm(cwd, { recursive: true, force: true });
182
+ }
183
+ });
184
+ it("does not crash Stop hook dispatch when the exec follow-up queue is malformed", async () => {
185
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-stop-exec-followup-corrupt-"));
186
+ try {
187
+ const session = await writeSessionStart(cwd, "sess-exec-followup-corrupt");
188
+ const queuePath = join(cwd, ".omx", "state", "sessions", session.session_id, "exec-followups.json");
189
+ await mkdir(dirname(queuePath), { recursive: true });
190
+ await writeFile(queuePath, '{"version":1,"records":[', "utf-8");
191
+ const result = await dispatchCodexNativeHook({
192
+ hook_event_name: "Stop",
193
+ cwd,
194
+ session_id: session.session_id,
195
+ });
196
+ assert.equal(result.hookEventName, "Stop");
197
+ assert.equal(result.outputJson, null);
198
+ const queueDirEntries = await readdir(dirname(queuePath));
199
+ assert.ok(queueDirEntries.some((entry) => entry.startsWith("exec-followups.json.corrupt-")));
200
+ const auditPath = join(cwd, ".omx", "logs", `exec-followups-${new Date().toISOString().slice(0, 10)}.jsonl`);
201
+ assert.match(await readFile(auditPath, "utf-8"), /exec_followup_queue_corrupt_recovered/);
202
+ }
203
+ finally {
204
+ await rm(cwd, { recursive: true, force: true });
205
+ }
206
+ });
207
+ it("emits exactly one parseable JSON object for active Stop CLI continuation", async () => {
208
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-json-"));
209
+ try {
210
+ await writeActiveAutopilotSession(cwd, "sess-cli-stop-json");
211
+ const stdout = runNativeHookCli({
212
+ hook_event_name: "Stop",
213
+ cwd,
214
+ session_id: "sess-cli-stop-json",
215
+ thread_id: "thread-cli-stop-json",
216
+ turn_id: "turn-cli-stop-json",
217
+ }, { cwd });
218
+ const output = parseSingleJsonStdout(stdout);
219
+ assert.equal(output.decision, "block");
220
+ assert.equal(output.stopReason, "autopilot_execution");
221
+ }
222
+ finally {
223
+ await rm(cwd, { recursive: true, force: true });
224
+ }
225
+ });
226
+ it("keeps noisy Stop hook plugin stdout out of native Stop CLI stdout", async () => {
227
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noisy-plugin-"));
228
+ try {
229
+ await writeActiveAutopilotSession(cwd, "sess-cli-stop-noisy-plugin");
230
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
231
+ await writeFile(join(cwd, ".omx", "hooks", "noisy.mjs"), `export async function onHookEvent(event) {
232
+ if (event.event === "stop") console.log("PLUGIN_NOISE");
233
+ }
234
+ `, "utf-8");
235
+ const stdout = runNativeHookCli({
236
+ hook_event_name: "Stop",
237
+ cwd,
238
+ session_id: "sess-cli-stop-noisy-plugin",
239
+ thread_id: "thread-cli-stop-noisy-plugin",
240
+ turn_id: "turn-cli-stop-noisy-plugin",
241
+ }, { cwd });
242
+ assert.doesNotMatch(stdout, /PLUGIN_NOISE/);
243
+ const output = parseSingleJsonStdout(stdout);
244
+ assert.equal(output.decision, "block");
245
+ assert.equal(output.stopReason, "autopilot_execution");
246
+ }
247
+ finally {
248
+ await rm(cwd, { recursive: true, force: true });
249
+ }
250
+ });
251
+ it("emits deterministic Stop JSON stdout when Stop dispatch fails", async () => {
252
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-dispatch-failure-"));
253
+ try {
254
+ const stdout = runNativeHookCli({
255
+ hook_event_name: "Stop",
256
+ cwd,
257
+ session_id: "sess-cli-stop-dispatch-failure",
258
+ thread_id: "thread-cli-stop-dispatch-failure",
259
+ turn_id: "turn-cli-stop-dispatch-failure",
260
+ }, {
261
+ cwd,
262
+ env: {
263
+ ...process.env,
264
+ NODE_ENV: "test",
265
+ OMX_NATIVE_HOOK_TEST_THROW_STOP_DISPATCH: "1",
266
+ },
267
+ });
268
+ const output = parseSingleJsonStdout(stdout);
269
+ assert.equal(output.decision, "block");
270
+ assert.equal(output.stopReason, "native_stop_dispatch_failure");
271
+ assert.match(String(output.reason), /failed before normal continuation handling/);
272
+ assert.match(String(output.systemMessage), /test-induced Stop dispatch failure/);
273
+ }
274
+ finally {
275
+ await rm(cwd, { recursive: true, force: true });
276
+ }
277
+ });
135
278
  it("maps Codex events onto OMX logical surfaces", () => {
136
279
  assert.equal(mapCodexHookEventToOmxEvent("SessionStart"), "session-start");
137
280
  assert.equal(mapCodexHookEventToOmxEvent("UserPromptSubmit"), "keyword-detector");
@@ -139,7 +282,7 @@ describe("codex native hook dispatch", () => {
139
282
  assert.equal(mapCodexHookEventToOmxEvent("PostToolUse"), "post-tool-use");
140
283
  assert.equal(mapCodexHookEventToOmxEvent("Stop"), "stop");
141
284
  });
142
- it("writes SessionStart state against the long-lived session owner pid and stays quiet for clean sessions", async () => {
285
+ it("writes SessionStart state against the long-lived session owner pid and injects environment context", async () => {
143
286
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-start-"));
144
287
  try {
145
288
  const result = await dispatchCodexNativeHook({
@@ -151,7 +294,11 @@ describe("codex native hook dispatch", () => {
151
294
  sessionOwnerPid: 43210,
152
295
  });
153
296
  assert.equal(result.omxEventName, "session-start");
154
- assert.equal(result.outputJson, null);
297
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
298
+ assert.match(additionalContext, /\[Execution environment\]/);
299
+ assert.match(additionalContext, /native-hook \/ Codex App outside tmux/);
300
+ assert.match(additionalContext, /omx team, omx hud, and omx quest(?:ion) need an attached tmux OMX CLI shell|omx team and omx hud need an attached tmux OMX CLI shell/);
301
+ assert.match(additionalContext, /not available from this outside-tmux surface/);
155
302
  const sessionState = JSON.parse(await readFile(join(cwd, ".omx", "state", "session.json"), "utf-8"));
156
303
  assert.equal(sessionState.session_id, "sess-start-1");
157
304
  assert.equal(sessionState.native_session_id, "sess-start-1");
@@ -203,6 +350,172 @@ describe("codex native hook dispatch", () => {
203
350
  await rm(cwd, { recursive: true, force: true });
204
351
  }
205
352
  });
353
+ it("keeps subagent SessionStart from replacing the canonical leader session", async () => {
354
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-"));
355
+ try {
356
+ const stateDir = join(cwd, ".omx", "state");
357
+ const canonicalSessionId = "omx-leader-session";
358
+ const leaderNativeSessionId = "codex-leader-thread";
359
+ const childNativeSessionId = "codex-child-thread";
360
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
361
+ await writeSessionStart(cwd, canonicalSessionId, {
362
+ nativeSessionId: leaderNativeSessionId,
363
+ });
364
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
365
+ active: true,
366
+ mode: "ralph",
367
+ current_phase: "executing",
368
+ iteration: 1,
369
+ max_iterations: 5,
370
+ });
371
+ const transcriptPath = join(cwd, "subagent-rollout.jsonl");
372
+ await writeFile(transcriptPath, `${JSON.stringify({
373
+ type: "session_meta",
374
+ payload: {
375
+ id: childNativeSessionId,
376
+ source: {
377
+ subagent: {
378
+ thread_spawn: {
379
+ parent_thread_id: leaderNativeSessionId,
380
+ depth: 1,
381
+ agent_nickname: "Hegel",
382
+ agent_role: "critic",
383
+ },
384
+ },
385
+ },
386
+ agent_nickname: "Hegel",
387
+ agent_role: "critic",
388
+ },
389
+ })}\n`);
390
+ const result = await dispatchCodexNativeHook({
391
+ hook_event_name: "SessionStart",
392
+ cwd,
393
+ session_id: childNativeSessionId,
394
+ transcript_path: transcriptPath,
395
+ }, { cwd, sessionOwnerPid: process.pid });
396
+ const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
397
+ assert.equal(sessionState.session_id, canonicalSessionId);
398
+ assert.equal(sessionState.native_session_id, leaderNativeSessionId);
399
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralph-state.json")), false);
400
+ assert.ok(result.outputJson);
401
+ const leaderRalph = JSON.parse(await readFile(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), "utf-8"));
402
+ assert.equal(leaderRalph.active, true);
403
+ assert.equal(leaderRalph.current_phase, "executing");
404
+ const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
405
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, leaderNativeSessionId);
406
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
407
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
408
+ assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
409
+ assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
410
+ assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
411
+ }
412
+ finally {
413
+ await rm(cwd, { recursive: true, force: true });
414
+ }
415
+ });
416
+ it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
417
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
418
+ try {
419
+ const stateDir = join(cwd, ".omx", "state");
420
+ const canonicalSessionId = "omx-leader-session-a";
421
+ const leaderNativeSessionId = "codex-leader-thread-a";
422
+ const unrelatedParentNativeSessionId = "codex-leader-thread-b";
423
+ const childNativeSessionId = "codex-child-thread-b";
424
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
425
+ await writeSessionStart(cwd, canonicalSessionId, {
426
+ nativeSessionId: leaderNativeSessionId,
427
+ });
428
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
429
+ active: true,
430
+ mode: "ralph",
431
+ current_phase: "executing",
432
+ iteration: 1,
433
+ max_iterations: 5,
434
+ });
435
+ const transcriptPath = join(cwd, "unrelated-subagent-rollout.jsonl");
436
+ await writeFile(transcriptPath, `${JSON.stringify({
437
+ type: "session_meta",
438
+ payload: {
439
+ id: childNativeSessionId,
440
+ source: {
441
+ subagent: {
442
+ thread_spawn: {
443
+ parent_thread_id: unrelatedParentNativeSessionId,
444
+ depth: 1,
445
+ agent_nickname: "Spinoza",
446
+ agent_role: "critic",
447
+ },
448
+ },
449
+ },
450
+ agent_nickname: "Spinoza",
451
+ agent_role: "critic",
452
+ },
453
+ })}\n`);
454
+ const result = await dispatchCodexNativeHook({
455
+ hook_event_name: "SessionStart",
456
+ cwd,
457
+ session_id: childNativeSessionId,
458
+ transcript_path: transcriptPath,
459
+ }, { cwd, sessionOwnerPid: process.pid });
460
+ const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
461
+ assert.equal(sessionState.session_id, canonicalSessionId);
462
+ assert.equal(sessionState.native_session_id, leaderNativeSessionId);
463
+ assert.equal(existsSync(join(stateDir, "subagent-tracking.json")), false);
464
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId)), false);
465
+ assert.equal(result.outputJson, null);
466
+ const leaderRalph = JSON.parse(await readFile(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), "utf-8"));
467
+ assert.equal(leaderRalph.active, true);
468
+ assert.equal(leaderRalph.current_phase, "executing");
469
+ }
470
+ finally {
471
+ await rm(cwd, { recursive: true, force: true });
472
+ }
473
+ });
474
+ it("describes attached tmux runtime in SessionStart context when TMUX is present", async () => {
475
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-start-tmux-"));
476
+ process.env.TMUX = "/tmp/tmux-attached";
477
+ process.env.TMUX_PANE = "%11";
478
+ try {
479
+ const result = await dispatchCodexNativeHook({
480
+ hook_event_name: "SessionStart",
481
+ cwd,
482
+ session_id: "sess-start-tmux-1",
483
+ }, {
484
+ cwd,
485
+ sessionOwnerPid: process.pid,
486
+ });
487
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
488
+ assert.match(additionalContext, /\[Execution environment\]/);
489
+ assert.match(additionalContext, /attached tmux runtime/);
490
+ assert.match(additionalContext, /omx team, omx hud, and omx quest(?:ion) are directly usable in this session/);
491
+ assert.match(additionalContext, /visible renderer available from the current pane/);
492
+ }
493
+ finally {
494
+ await rm(cwd, { recursive: true, force: true });
495
+ }
496
+ });
497
+ it("describes direct CLI outside tmux in SessionStart context when the launch source is cli", async () => {
498
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-start-cli-"));
499
+ try {
500
+ const result = await dispatchCodexNativeHook({
501
+ hook_event_name: "SessionStart",
502
+ cwd,
503
+ session_id: "sess-start-cli-1",
504
+ source: "cli",
505
+ }, {
506
+ cwd,
507
+ sessionOwnerPid: process.pid,
508
+ });
509
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
510
+ assert.match(additionalContext, /\[Execution environment\]/);
511
+ assert.match(additionalContext, /direct CLI outside tmux/);
512
+ assert.doesNotMatch(additionalContext, /native-hook \/ Codex App outside tmux/);
513
+ assert.match(additionalContext, /omx team, omx hud, and omx quest(?:ion) need an attached tmux OMX CLI shell|omx team and omx hud need an attached tmux OMX CLI shell/);
514
+ }
515
+ finally {
516
+ await rm(cwd, { recursive: true, force: true });
517
+ }
518
+ });
206
519
  it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
207
520
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
208
521
  try {
@@ -393,6 +706,8 @@ describe("codex native hook dispatch", () => {
393
706
  assert.equal(sessionState.session_id, "codex-native-new");
394
707
  assert.equal(sessionState.native_session_id, "codex-native-new");
395
708
  const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
709
+ assert.match(additionalContext, /\[Execution environment\]/);
710
+ assert.match(additionalContext, /native-hook \/ Codex App outside tmux/);
396
711
  assert.match(additionalContext, /\[Priority notes\]/);
397
712
  assert.match(additionalContext, /Preserve durable project guidance/);
398
713
  assert.doesNotMatch(additionalContext, /stale UI rework context snapshot/);
@@ -448,6 +763,29 @@ describe("codex native hook dispatch", () => {
448
763
  await rm(cwd, { recursive: true, force: true });
449
764
  }
450
765
  });
766
+ it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
767
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
768
+ try {
769
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
770
+ const result = await dispatchCodexNativeHook({
771
+ hook_event_name: "UserPromptSubmit",
772
+ cwd,
773
+ session_id: "sess-plugin-1",
774
+ thread_id: "thread-plugin-1",
775
+ turn_id: "turn-plugin-1",
776
+ prompt: "$oh-my-codex:ralplan implement issue #1307",
777
+ }, { cwd });
778
+ assert.equal(result.omxEventName, "keyword-detector");
779
+ assert.equal(result.skillState?.skill, "ralplan");
780
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
781
+ assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
782
+ assert.match(message, /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
783
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
784
+ }
785
+ finally {
786
+ await rm(cwd, { recursive: true, force: true });
787
+ }
788
+ });
451
789
  it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
452
790
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
453
791
  try {
@@ -596,6 +934,29 @@ describe("codex native hook dispatch", () => {
596
934
  await rm(cwd, { recursive: true, force: true });
597
935
  }
598
936
  });
937
+ it("clarifies that plugin-prefixed prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
938
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-ralph-routing-"));
939
+ try {
940
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
941
+ const result = await dispatchCodexNativeHook({
942
+ hook_event_name: "UserPromptSubmit",
943
+ cwd,
944
+ session_id: "sess-plugin-ralph-msg",
945
+ thread_id: "thread-plugin-ralph-msg",
946
+ turn_id: "turn-plugin-ralph-msg",
947
+ prompt: "$oh-my-codex:ralph continue verification",
948
+ }, { cwd });
949
+ assert.equal(result.omxEventName, "keyword-detector");
950
+ assert.equal(result.skillState?.skill, "ralph");
951
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
952
+ assert.match(message, /\$oh-my-codex:ralph" -> ralph/);
953
+ assert.match(message, /skill: ralph activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-ralph-msg\/ralph-state\.json; write subsequent updates via omx_state MCP\./);
954
+ assert.match(message, /Prompt-side `\$ralph` activation seeds Ralph workflow state only; it does not invoke `omx ralph`\./);
955
+ }
956
+ finally {
957
+ await rm(cwd, { recursive: true, force: true });
958
+ }
959
+ });
599
960
  it("keeps bare keep-going continuation on the active autopilot skill instead of denying with generic ralph overlap", async () => {
600
961
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-bare-continuation-"));
601
962
  try {
@@ -642,7 +1003,7 @@ describe("codex native hook dispatch", () => {
642
1003
  await rm(cwd, { recursive: true, force: true });
643
1004
  }
644
1005
  });
645
- it("clarifies that prompt-side deep-interview activation must use omx question", async () => {
1006
+ it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
646
1007
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
647
1008
  try {
648
1009
  await mkdir(join(cwd, ".omx", "state"), { recursive: true });
@@ -659,19 +1020,45 @@ describe("codex native hook dispatch", () => {
659
1020
  const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
660
1021
  assert.match(message, /\$deep-interview" -> deep-interview/);
661
1022
  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\./);
662
- assert.match(message, /Deep-interview must ask each interview round via `omx question`/);
663
- assert.match(message, /do not fall back to `request_user_input` or plain-text questioning/i);
664
- assert.match(message, /After starting `omx question` in a background terminal, wait for that terminal to finish and read the JSON answer before continuing the interview\./);
665
- assert.match(message, /If bare `omx question` is unavailable in this reused session, use the current-session CLI bridge command:/);
666
- assert.match(message, /'.+' '.+dist\/cli\/omx\.js' question/);
1023
+ assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
1024
+ assert.match(message, /Do not invoke `omx question`, `omx hud`, or `omx team`/);
1025
+ assert.match(message, /native structured question tool when available/);
1026
+ assert.match(message, /ask exactly one concise plain-text question/);
1027
+ assert.match(message, /no tmux question obligation should be created outside tmux/);
667
1028
  assert.doesNotMatch(message, /OMX_QUESTION_RETURN_PANE=/);
668
1029
  assert.doesNotMatch(message, /preserve the leader pane/i);
669
- assert.match(message, /Stop remains blocked while a deep-interview question obligation is pending\./);
670
1030
  }
671
1031
  finally {
672
1032
  await rm(cwd, { recursive: true, force: true });
673
1033
  }
674
1034
  });
1035
+ it("uses native fallback deep-interview guidance on Windows outside tmux", async () => {
1036
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-win32-"));
1037
+ const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
1038
+ try {
1039
+ Object.defineProperty(process, "platform", { value: "win32", configurable: true });
1040
+ const result = await dispatchCodexNativeHook({
1041
+ hook_event_name: "UserPromptSubmit",
1042
+ cwd,
1043
+ session_id: "sess-deep-interview-msg-win32",
1044
+ thread_id: "thread-deep-interview-msg-win32",
1045
+ turn_id: "turn-deep-interview-msg-win32",
1046
+ prompt: "$deep-interview gather requirements",
1047
+ }, { cwd });
1048
+ assert.equal(result.omxEventName, "keyword-detector");
1049
+ assert.equal(result.skillState?.skill, "deep-interview");
1050
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
1051
+ assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
1052
+ assert.match(message, /native structured question tool when available/);
1053
+ assert.doesNotMatch(message, /OMX_QUESTION_RETURN_PANE=/);
1054
+ assert.doesNotMatch(message, /current-session CLI bridge command/);
1055
+ }
1056
+ finally {
1057
+ if (originalPlatform)
1058
+ Object.defineProperty(process, "platform", originalPlatform);
1059
+ await rm(cwd, { recursive: true, force: true });
1060
+ }
1061
+ });
675
1062
  it("includes leader-pane preservation guidance when a pane hint is available", async () => {
676
1063
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-pane-hint-"));
677
1064
  try {
@@ -697,11 +1084,51 @@ describe("codex native hook dispatch", () => {
697
1084
  assert.equal(result.omxEventName, "keyword-detector");
698
1085
  assert.equal(result.skillState?.skill, "deep-interview");
699
1086
  const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
700
- assert.match(message, /OMX_QUESTION_RETURN_PANE='%77'/);
701
- assert.match(message, /preserve the leader pane/i);
702
- assert.match(message, /OMX_QUESTION_RETURN_PANE=%77/);
1087
+ assert.match(message, /not attached to tmux/);
1088
+ assert.match(message, /native structured question tool when available/);
1089
+ assert.match(message, /tmux return bridge \(%77\) is recorded/);
1090
+ assert.doesNotMatch(message, /current-session CLI bridge command/);
1091
+ }
1092
+ finally {
1093
+ await rm(cwd, { recursive: true, force: true });
1094
+ }
1095
+ });
1096
+ it("uses native fallback guidance on Windows when a pane hint is available", async () => {
1097
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-pane-hint-win32-"));
1098
+ const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
1099
+ try {
1100
+ Object.defineProperty(process, "platform", { value: "win32", configurable: true });
1101
+ const sessionId = "sess-deep-interview-pane-hint-win32";
1102
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
1103
+ await mkdir(sessionDir, { recursive: true });
1104
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
1105
+ active: true,
1106
+ mode: "deep-interview",
1107
+ current_phase: "intent-first",
1108
+ started_at: "2026-04-21T10:00:00.000Z",
1109
+ updated_at: "2026-04-21T10:00:00.000Z",
1110
+ tmux_pane_id: "%77",
1111
+ });
1112
+ const result = await dispatchCodexNativeHook({
1113
+ hook_event_name: "UserPromptSubmit",
1114
+ cwd,
1115
+ session_id: sessionId,
1116
+ thread_id: "thread-deep-interview-pane-hint-win32",
1117
+ turn_id: "turn-deep-interview-pane-hint-win32",
1118
+ prompt: "$deep-interview gather requirements",
1119
+ }, { cwd });
1120
+ assert.equal(result.omxEventName, "keyword-detector");
1121
+ assert.equal(result.skillState?.skill, "deep-interview");
1122
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
1123
+ assert.match(message, /not attached to tmux/);
1124
+ assert.match(message, /native structured question tool when available/);
1125
+ assert.match(message, /tmux return bridge \(%77\) is recorded/);
1126
+ assert.doesNotMatch(message, /OMX_QUESTION_RETURN_PANE=/);
1127
+ assert.doesNotMatch(message, /PowerShell\/background-terminal/);
703
1128
  }
704
1129
  finally {
1130
+ if (originalPlatform)
1131
+ Object.defineProperty(process, "platform", originalPlatform);
705
1132
  await rm(cwd, { recursive: true, force: true });
706
1133
  }
707
1134
  });
@@ -840,13 +1267,35 @@ export async function onHookEvent(event) {
840
1267
  await rm(cwd, { recursive: true, force: true });
841
1268
  }
842
1269
  });
843
- it("nudges $team prompt-submit routing toward omx team runtime usage", async () => {
844
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-"));
1270
+ it("does not emit UserPromptSubmit routing context for unknown plugin-prefixed $tokens", async () => {
1271
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-unknown-plugin-token-"));
845
1272
  try {
846
1273
  await mkdir(join(cwd, ".omx", "state"), { recursive: true });
847
1274
  const result = await dispatchCodexNativeHook({
848
1275
  hook_event_name: "UserPromptSubmit",
849
1276
  cwd,
1277
+ session_id: "sess-unknown-plugin-1",
1278
+ thread_id: "thread-unknown-plugin-1",
1279
+ turn_id: "turn-unknown-plugin-1",
1280
+ prompt: "$oh-my-codex:maer-thinking 다시 설명해봐",
1281
+ }, { cwd });
1282
+ assert.equal(result.omxEventName, "keyword-detector");
1283
+ assert.equal(result.skillState, null);
1284
+ assert.equal(result.outputJson, null);
1285
+ assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
1286
+ }
1287
+ finally {
1288
+ await rm(cwd, { recursive: true, force: true });
1289
+ }
1290
+ });
1291
+ it("denies direct $team prompt activation from Codex App/native outside tmux", async () => {
1292
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-native-block-"));
1293
+ try {
1294
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1295
+ const result = await dispatchCodexNativeHook({
1296
+ hook_event_name: "UserPromptSubmit",
1297
+ cwd,
1298
+ source: "codex-app",
850
1299
  session_id: "sess-team-1",
851
1300
  thread_id: "thread-team-1",
852
1301
  turn_id: "turn-team-1",
@@ -854,53 +1303,125 @@ export async function onHookEvent(event) {
854
1303
  }, { cwd });
855
1304
  assert.equal(result.omxEventName, "keyword-detector");
856
1305
  assert.equal(result.skillState?.skill, "team");
857
- assert.match(JSON.stringify(result.outputJson), /skill: team activated and initial state initialized at \.omx\/state\/team-state\.json; write subsequent updates via omx_state MCP\./);
858
- assert.match(JSON.stringify(result.outputJson), /Use the durable OMX team runtime via `omx team \.\.\.`/);
859
- assert.match(JSON.stringify(result.outputJson), /If you need runtime syntax, run `omx team --help` yourself\./);
860
- const state = JSON.parse(await readFile(join(cwd, ".omx", "state", "team-state.json"), "utf-8"));
861
- assert.equal(state.mode, "team");
862
- assert.equal(state.active, true);
863
- assert.equal(state.current_phase, "starting");
1306
+ assert.equal(result.skillState?.active, false);
1307
+ assert.match(String(result.skillState?.transition_error || ""), /cannot activate the tmux-only `team` workflow directly/);
1308
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
1309
+ assert.match(message, /denied workflow keyword "\$team" -> team/);
1310
+ assert.match(message, /attached tmux shell first/);
1311
+ assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
864
1312
  }
865
1313
  finally {
866
1314
  await rm(cwd, { recursive: true, force: true });
867
1315
  }
868
1316
  });
869
- it("returns actionable denial guidance for unsupported workflow overlaps on prompt submit", async () => {
870
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-transition-deny-"));
1317
+ it("still denies direct $team prompt activation from Codex App/native outside tmux when a tmux return bridge exists", async () => {
1318
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-native-bridge-block-"));
871
1319
  try {
872
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
873
- await dispatchCodexNativeHook({
874
- hook_event_name: "UserPromptSubmit",
875
- cwd,
876
- session_id: "sess-deny-1",
877
- thread_id: "thread-deny-1",
878
- turn_id: "turn-deny-1",
879
- prompt: "$team ship this fix",
880
- }, { cwd });
881
- const denied = await dispatchCodexNativeHook({
1320
+ await mkdir(join(cwd, ".omx", "state", "sessions", "sess-team-bridge"), { recursive: true });
1321
+ await writeJson(join(cwd, ".omx", "state", "sessions", "sess-team-bridge", "ralph-state.json"), {
1322
+ mode: "ralph",
1323
+ active: true,
1324
+ tmux_pane_id: "%42",
1325
+ });
1326
+ const result = await dispatchCodexNativeHook({
882
1327
  hook_event_name: "UserPromptSubmit",
883
1328
  cwd,
884
- session_id: "sess-deny-1",
885
- thread_id: "thread-deny-1",
886
- turn_id: "turn-deny-2",
887
- prompt: "$autopilot also run this",
1329
+ source: "codex-app",
1330
+ session_id: "sess-team-bridge",
1331
+ thread_id: "thread-team-bridge",
1332
+ turn_id: "turn-team-bridge",
1333
+ prompt: "$team ship this fix with verification",
888
1334
  }, { cwd });
889
- assert.match(JSON.stringify(denied.outputJson), /denied workflow keyword/i);
890
- assert.match(JSON.stringify(denied.outputJson), /Unsupported workflow overlap: team \+ autopilot\./);
891
- assert.match(JSON.stringify(denied.outputJson), /`omx state clear --mode <mode>`/);
892
- assert.match(JSON.stringify(denied.outputJson), /`omx_state\.\*` MCP tools/);
893
- assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-deny-1", "autopilot-state.json")), false);
1335
+ assert.equal(result.omxEventName, "keyword-detector");
1336
+ assert.equal(result.skillState?.skill, "team");
1337
+ assert.equal(result.skillState?.active, false);
1338
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
1339
+ assert.match(message, /attached tmux shell first/);
1340
+ assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
894
1341
  }
895
1342
  finally {
896
1343
  await rm(cwd, { recursive: true, force: true });
897
1344
  }
898
1345
  });
899
- it("surfaces transition success output for allowlisted prompt-submit handoffs", async () => {
900
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-transition-success-"));
1346
+ it("keeps direct CLI outside-tmux $team prompt guidance compatible with manual shell launch", async () => {
1347
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-cli-guidance-"));
901
1348
  try {
902
- const sessionDir = join(cwd, ".omx", "state", "sessions", "sess-handoff-1");
903
- await mkdir(sessionDir, { recursive: true });
1349
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1350
+ const result = await dispatchCodexNativeHook({
1351
+ hook_event_name: "UserPromptSubmit",
1352
+ cwd,
1353
+ source: "cli",
1354
+ session_id: "sess-team-cli-guidance",
1355
+ thread_id: "thread-team-cli-guidance",
1356
+ turn_id: "turn-team-cli-guidance",
1357
+ prompt: "$team ship this fix with verification",
1358
+ }, { cwd });
1359
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
1360
+ assert.match(message, /run `omx team \.\.\.` yourself from shell/);
1361
+ assert.doesNotMatch(message, /not directly available here/);
1362
+ }
1363
+ finally {
1364
+ await rm(cwd, { recursive: true, force: true });
1365
+ }
1366
+ });
1367
+ it("keeps $team prompt-submit routing directly tmux-capable when already inside tmux", async () => {
1368
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-tmux-"));
1369
+ process.env.TMUX = "/tmp/tmux-live";
1370
+ process.env.TMUX_PANE = "%5";
1371
+ try {
1372
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1373
+ const result = await dispatchCodexNativeHook({
1374
+ hook_event_name: "UserPromptSubmit",
1375
+ cwd,
1376
+ session_id: "sess-team-tmux-1",
1377
+ thread_id: "thread-team-tmux-1",
1378
+ turn_id: "turn-team-tmux-1",
1379
+ prompt: "$team ship this fix with verification",
1380
+ }, { cwd });
1381
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
1382
+ assert.match(message, /Use the durable OMX team runtime via `omx team \.\.\.`/);
1383
+ assert.match(message, /run `omx team --help` yourself/);
1384
+ assert.doesNotMatch(message, /not directly available here/);
1385
+ }
1386
+ finally {
1387
+ await rm(cwd, { recursive: true, force: true });
1388
+ }
1389
+ });
1390
+ it("returns actionable denial guidance for unsupported workflow overlaps on prompt submit", async () => {
1391
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-transition-deny-"));
1392
+ try {
1393
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1394
+ await dispatchCodexNativeHook({
1395
+ hook_event_name: "UserPromptSubmit",
1396
+ cwd,
1397
+ session_id: "sess-deny-1",
1398
+ thread_id: "thread-deny-1",
1399
+ turn_id: "turn-deny-1",
1400
+ prompt: "$team ship this fix",
1401
+ }, { cwd });
1402
+ const denied = await dispatchCodexNativeHook({
1403
+ hook_event_name: "UserPromptSubmit",
1404
+ cwd,
1405
+ session_id: "sess-deny-1",
1406
+ thread_id: "thread-deny-1",
1407
+ turn_id: "turn-deny-2",
1408
+ prompt: "$autopilot also run this",
1409
+ }, { cwd });
1410
+ assert.match(JSON.stringify(denied.outputJson), /denied workflow keyword/i);
1411
+ assert.match(JSON.stringify(denied.outputJson), /Unsupported workflow overlap: team \+ autopilot\./);
1412
+ assert.match(JSON.stringify(denied.outputJson), /`omx state clear --mode <mode>`/);
1413
+ assert.match(JSON.stringify(denied.outputJson), /`omx_state\.\*` MCP tools/);
1414
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-deny-1", "autopilot-state.json")), false);
1415
+ }
1416
+ finally {
1417
+ await rm(cwd, { recursive: true, force: true });
1418
+ }
1419
+ });
1420
+ it("surfaces transition success output for allowlisted prompt-submit handoffs", async () => {
1421
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-transition-success-"));
1422
+ try {
1423
+ const sessionDir = join(cwd, ".omx", "state", "sessions", "sess-handoff-1");
1424
+ await mkdir(sessionDir, { recursive: true });
904
1425
  await writeJson(join(sessionDir, "deep-interview-state.json"), {
905
1426
  active: true,
906
1427
  mode: "deep-interview",
@@ -955,6 +1476,30 @@ export async function onHookEvent(event) {
955
1476
  await rm(cwd, { recursive: true, force: true });
956
1477
  }
957
1478
  });
1479
+ it("keeps the planning skill active for mixed plugin-prefixed and bare workflow invocations together", async () => {
1480
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-planning-precedence-"));
1481
+ try {
1482
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1483
+ const result = await dispatchCodexNativeHook({
1484
+ hook_event_name: "UserPromptSubmit",
1485
+ cwd,
1486
+ session_id: "sess-plugin-multi-1",
1487
+ thread_id: "thread-plugin-multi-1",
1488
+ turn_id: "turn-plugin-multi-1",
1489
+ prompt: "$oh-my-codex:ralplan $team $oh-my-codex:ralph ship this fix",
1490
+ }, { cwd });
1491
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || '');
1492
+ assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
1493
+ assert.match(message, /\$team" -> team/);
1494
+ assert.match(message, /\$oh-my-codex:ralph" -> ralph/);
1495
+ assert.doesNotMatch(message, /mode transiting:/);
1496
+ assert.match(message, /planning preserved over simultaneous execution follow-up; deferred skills: team, ralph\./);
1497
+ assert.match(message, /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-multi-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
1498
+ }
1499
+ finally {
1500
+ await rm(cwd, { recursive: true, force: true });
1501
+ }
1502
+ });
958
1503
  it("runs prompt-submit HUD reconciliation as a best-effort tmux-only side effect", async () => {
959
1504
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-"));
960
1505
  const originalTmux = process.env.TMUX;
@@ -1072,6 +1617,99 @@ esac
1072
1617
  await rm(cwd, { recursive: true, force: true });
1073
1618
  }
1074
1619
  });
1620
+ it("allows PowerShell env bridge forms for omx question return panes", async () => {
1621
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-powershell-allow-"));
1622
+ try {
1623
+ const commands = [
1624
+ `$env:OMX_QUESTION_RETURN_PANE=$env:TMUX_PANE; omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'`,
1625
+ `$env:OMX_QUESTION_RETURN_PANE='%42'; node ./dist/cli/omx.js question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'`,
1626
+ `$env:OMX_LEADER_PANE_ID="%43"; omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'`,
1627
+ ];
1628
+ for (const [index, command] of commands.entries()) {
1629
+ const result = await dispatchCodexNativeHook({
1630
+ hook_event_name: "PreToolUse",
1631
+ cwd,
1632
+ tool_name: "Bash",
1633
+ tool_use_id: `tool-question-powershell-allow-${index}`,
1634
+ tool_input: { command },
1635
+ }, { cwd });
1636
+ assert.equal(result.omxEventName, "pre-tool-use");
1637
+ assert.equal(result.outputJson, null);
1638
+ }
1639
+ }
1640
+ finally {
1641
+ await rm(cwd, { recursive: true, force: true });
1642
+ }
1643
+ });
1644
+ it("allows Bash omx question when a valid inherited OMX_QUESTION_RETURN_PANE bridge is already exported", async () => {
1645
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-env-allow-"));
1646
+ const originalReturnPane = process.env.OMX_QUESTION_RETURN_PANE;
1647
+ try {
1648
+ process.env.OMX_QUESTION_RETURN_PANE = "%42";
1649
+ const result = await dispatchCodexNativeHook({
1650
+ hook_event_name: "PreToolUse",
1651
+ cwd,
1652
+ tool_name: "Bash",
1653
+ tool_use_id: "tool-question-env-allow",
1654
+ tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
1655
+ }, { cwd });
1656
+ assert.equal(result.omxEventName, "pre-tool-use");
1657
+ assert.equal(result.outputJson, null);
1658
+ }
1659
+ finally {
1660
+ if (originalReturnPane === undefined)
1661
+ delete process.env.OMX_QUESTION_RETURN_PANE;
1662
+ else
1663
+ process.env.OMX_QUESTION_RETURN_PANE = originalReturnPane;
1664
+ await rm(cwd, { recursive: true, force: true });
1665
+ }
1666
+ });
1667
+ it("allows Bash omx question when a valid inherited OMX_LEADER_PANE_ID bridge is already exported", async () => {
1668
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-leader-env-allow-"));
1669
+ const originalLeaderPane = process.env.OMX_LEADER_PANE_ID;
1670
+ try {
1671
+ process.env.OMX_LEADER_PANE_ID = "%43";
1672
+ const result = await dispatchCodexNativeHook({
1673
+ hook_event_name: "PreToolUse",
1674
+ cwd,
1675
+ tool_name: "Bash",
1676
+ tool_use_id: "tool-question-leader-env-allow",
1677
+ tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
1678
+ }, { cwd });
1679
+ assert.equal(result.omxEventName, "pre-tool-use");
1680
+ assert.equal(result.outputJson, null);
1681
+ }
1682
+ finally {
1683
+ if (originalLeaderPane === undefined)
1684
+ delete process.env.OMX_LEADER_PANE_ID;
1685
+ else
1686
+ process.env.OMX_LEADER_PANE_ID = originalLeaderPane;
1687
+ await rm(cwd, { recursive: true, force: true });
1688
+ }
1689
+ });
1690
+ it("still blocks Bash omx question when an inherited OMX_QUESTION_RETURN_PANE value is malformed", async () => {
1691
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-env-malformed-"));
1692
+ const originalReturnPane = process.env.OMX_QUESTION_RETURN_PANE;
1693
+ try {
1694
+ process.env.OMX_QUESTION_RETURN_PANE = "not-a-pane";
1695
+ const result = await dispatchCodexNativeHook({
1696
+ hook_event_name: "PreToolUse",
1697
+ cwd,
1698
+ tool_name: "Bash",
1699
+ tool_use_id: "tool-question-env-malformed",
1700
+ tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
1701
+ }, { cwd });
1702
+ assert.equal(result.omxEventName, "pre-tool-use");
1703
+ assert.equal(result.outputJson?.decision, "block");
1704
+ }
1705
+ finally {
1706
+ if (originalReturnPane === undefined)
1707
+ delete process.env.OMX_QUESTION_RETURN_PANE;
1708
+ else
1709
+ process.env.OMX_QUESTION_RETURN_PANE = originalReturnPane;
1710
+ await rm(cwd, { recursive: true, force: true });
1711
+ }
1712
+ });
1075
1713
  it("blocks Bash node omx.js question when the command does not preserve the leader-pane return hint", async () => {
1076
1714
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-node-block-"));
1077
1715
  try {
@@ -1089,6 +1727,173 @@ esac
1089
1727
  await rm(cwd, { recursive: true, force: true });
1090
1728
  }
1091
1729
  });
1730
+ it("blocks native/App Bash omx question with bridge-specific outside-tmux guidance", async () => {
1731
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-native-block-"));
1732
+ try {
1733
+ const result = await dispatchCodexNativeHook({
1734
+ hook_event_name: "PreToolUse",
1735
+ cwd,
1736
+ source: "codex-app",
1737
+ session_id: "sess-question-native-block",
1738
+ tool_name: "Bash",
1739
+ tool_use_id: "tool-question-native-block",
1740
+ tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
1741
+ }, { cwd });
1742
+ assert.equal(result.omxEventName, "pre-tool-use");
1743
+ assert.equal(result.outputJson?.decision, "block");
1744
+ assert.equal(result.outputJson?.hookSpecificOutput, undefined);
1745
+ assert.match(String(result.outputJson?.reason || ""), /Codex App\/native outside-tmux Bash sessions/);
1746
+ assert.match(String(result.outputJson?.systemMessage || ""), /native structured question tool/);
1747
+ }
1748
+ finally {
1749
+ await rm(cwd, { recursive: true, force: true });
1750
+ }
1751
+ });
1752
+ it("blocks native/App Bash omx question even when the command preserves a tmux return bridge", async () => {
1753
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-native-allow-"));
1754
+ try {
1755
+ const result = await dispatchCodexNativeHook({
1756
+ hook_event_name: "PreToolUse",
1757
+ cwd,
1758
+ source: "codex-app",
1759
+ session_id: "sess-question-native-bridge-block",
1760
+ tool_name: "Bash",
1761
+ tool_use_id: "tool-question-native-bridge-block",
1762
+ tool_input: { command: `OMX_QUESTION_RETURN_PANE=$TMUX_PANE omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
1763
+ }, { cwd });
1764
+ assert.equal(result.omxEventName, "pre-tool-use");
1765
+ assert.equal(result.outputJson?.decision, "block");
1766
+ assert.match(String(result.outputJson?.systemMessage || ""), /native structured question tool/);
1767
+ }
1768
+ finally {
1769
+ await rm(cwd, { recursive: true, force: true });
1770
+ }
1771
+ });
1772
+ it("blocks native/App Bash omx question when a valid inherited OMX_QUESTION_RETURN_PANE bridge is already exported", async () => {
1773
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-native-env-allow-"));
1774
+ const originalReturnPane = process.env.OMX_QUESTION_RETURN_PANE;
1775
+ try {
1776
+ process.env.OMX_QUESTION_RETURN_PANE = "%42";
1777
+ const result = await dispatchCodexNativeHook({
1778
+ hook_event_name: "PreToolUse",
1779
+ cwd,
1780
+ source: "codex-app",
1781
+ session_id: "sess-question-native-env-allow",
1782
+ tool_name: "Bash",
1783
+ tool_use_id: "tool-question-native-env-allow",
1784
+ tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
1785
+ }, { cwd });
1786
+ assert.equal(result.omxEventName, "pre-tool-use");
1787
+ assert.equal(result.outputJson?.decision, "block");
1788
+ }
1789
+ finally {
1790
+ if (originalReturnPane === undefined)
1791
+ delete process.env.OMX_QUESTION_RETURN_PANE;
1792
+ else
1793
+ process.env.OMX_QUESTION_RETURN_PANE = originalReturnPane;
1794
+ await rm(cwd, { recursive: true, force: true });
1795
+ }
1796
+ });
1797
+ it("blocks Bash omx hud from Codex App/native outside tmux without PreToolUse additionalContext", async () => {
1798
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-hud-native-block-"));
1799
+ try {
1800
+ const result = await dispatchCodexNativeHook({
1801
+ hook_event_name: "PreToolUse",
1802
+ cwd,
1803
+ source: "codex-app",
1804
+ session_id: "sess-hud-native-block",
1805
+ tool_name: "Bash",
1806
+ tool_use_id: "tool-hud-native-block",
1807
+ tool_input: { command: "omx hud --tmux" },
1808
+ }, { cwd });
1809
+ assert.equal(result.omxEventName, "pre-tool-use");
1810
+ assert.equal(result.outputJson?.decision, "block");
1811
+ assert.equal(result.outputJson?.hookSpecificOutput, undefined);
1812
+ assert.match(String(result.outputJson?.systemMessage || ""), /attached tmux shell first/);
1813
+ }
1814
+ finally {
1815
+ await rm(cwd, { recursive: true, force: true });
1816
+ }
1817
+ });
1818
+ it("blocks Bash omx team from Codex App/native outside tmux", async () => {
1819
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-native-block-"));
1820
+ try {
1821
+ const result = await dispatchCodexNativeHook({
1822
+ hook_event_name: "PreToolUse",
1823
+ cwd,
1824
+ source: "codex-app",
1825
+ session_id: "sess-team-native-block",
1826
+ tool_name: "Bash",
1827
+ tool_use_id: "tool-team-native-block",
1828
+ tool_input: { command: "omx team status my-team" },
1829
+ }, { cwd });
1830
+ assert.equal(result.omxEventName, "pre-tool-use");
1831
+ assert.equal(result.outputJson?.decision, "block");
1832
+ assert.equal(result.outputJson?.hookSpecificOutput, undefined);
1833
+ assert.match(String(result.outputJson?.reason || ""), /cannot be launched directly from Codex App\/native outside-tmux Bash sessions/);
1834
+ assert.match(String(result.outputJson?.systemMessage || ""), /launch OMX CLI from an attached tmux shell first/);
1835
+ }
1836
+ finally {
1837
+ await rm(cwd, { recursive: true, force: true });
1838
+ }
1839
+ });
1840
+ it("blocks Bash node omx.js team from Codex App/native outside tmux", async () => {
1841
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-node-native-block-"));
1842
+ try {
1843
+ const result = await dispatchCodexNativeHook({
1844
+ hook_event_name: "PreToolUse",
1845
+ cwd,
1846
+ source: "codex-app",
1847
+ session_id: "sess-team-node-native-block",
1848
+ tool_name: "Bash",
1849
+ tool_use_id: "tool-team-node-native-block",
1850
+ tool_input: { command: "node ./dist/cli/omx.js team status my-team" },
1851
+ }, { cwd });
1852
+ assert.equal(result.omxEventName, "pre-tool-use");
1853
+ assert.equal(result.outputJson?.decision, "block");
1854
+ assert.match(String(result.outputJson?.systemMessage || ""), /Codex App\/native outside-tmux sessions/);
1855
+ }
1856
+ finally {
1857
+ await rm(cwd, { recursive: true, force: true });
1858
+ }
1859
+ });
1860
+ it("preserves direct CLI outside-tmux omx team Bash behavior", async () => {
1861
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-cli-outside-"));
1862
+ try {
1863
+ const result = await dispatchCodexNativeHook({
1864
+ hook_event_name: "PreToolUse",
1865
+ cwd,
1866
+ source: "cli",
1867
+ session_id: "sess-team-cli-outside",
1868
+ tool_name: "Bash",
1869
+ tool_use_id: "tool-team-cli-outside",
1870
+ tool_input: { command: "omx team status my-team" },
1871
+ }, { cwd });
1872
+ assert.equal(result.omxEventName, "pre-tool-use");
1873
+ assert.equal(result.outputJson, null);
1874
+ }
1875
+ finally {
1876
+ await rm(cwd, { recursive: true, force: true });
1877
+ }
1878
+ });
1879
+ it("preserves source-less outside-tmux omx team Bash behavior when no native session evidence exists", async () => {
1880
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-cli-nosource-"));
1881
+ try {
1882
+ const result = await dispatchCodexNativeHook({
1883
+ hook_event_name: "PreToolUse",
1884
+ cwd,
1885
+ session_id: "sess-team-cli-nosource",
1886
+ tool_name: "Bash",
1887
+ tool_use_id: "tool-team-cli-nosource",
1888
+ tool_input: { command: "omx team status my-team" },
1889
+ }, { cwd });
1890
+ assert.equal(result.omxEventName, "pre-tool-use");
1891
+ assert.equal(result.outputJson, null);
1892
+ }
1893
+ finally {
1894
+ await rm(cwd, { recursive: true, force: true });
1895
+ }
1896
+ });
1092
1897
  it("returns a destructive-command caution on PreToolUse for rm -rf dist", async () => {
1093
1898
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-danger-"));
1094
1899
  try {
@@ -1128,7 +1933,7 @@ esac
1128
1933
  await rm(cwd, { recursive: true, force: true });
1129
1934
  }
1130
1935
  });
1131
- it("blocks PreToolUse git commit when the inline message is not Lore-compliant", async () => {
1936
+ it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
1132
1937
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
1133
1938
  try {
1134
1939
  const result = await dispatchCodexNativeHook({
@@ -1144,13 +1949,6 @@ esac
1144
1949
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1145
1950
  hookSpecificOutput: {
1146
1951
  hookEventName: "PreToolUse",
1147
- additionalContext: [
1148
- "Lore-format git commit enforcement triggered.",
1149
- "- Add a blank line after the subject before the narrative body.",
1150
- "- Add a narrative body paragraph explaining the decision context.",
1151
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1152
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1153
- ].join("\n"),
1154
1952
  },
1155
1953
  systemMessage: [
1156
1954
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1160,6 +1958,9 @@ esac
1160
1958
  "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1161
1959
  ].join("\n"),
1162
1960
  });
1961
+ const hookSpecificOutput = result.outputJson
1962
+ .hookSpecificOutput ?? {};
1963
+ assert.equal("additionalContext" in hookSpecificOutput, false);
1163
1964
  }
1164
1965
  finally {
1165
1966
  await rm(cwd, { recursive: true, force: true });
@@ -1232,13 +2033,6 @@ esac
1232
2033
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1233
2034
  hookSpecificOutput: {
1234
2035
  hookEventName: "PreToolUse",
1235
- additionalContext: [
1236
- "Lore-format git commit enforcement triggered.",
1237
- "- Add a blank line after the subject before the narrative body.",
1238
- "- Add a narrative body paragraph explaining the decision context.",
1239
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1240
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1241
- ].join("\n"),
1242
2036
  },
1243
2037
  systemMessage: [
1244
2038
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1269,13 +2063,6 @@ esac
1269
2063
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1270
2064
  hookSpecificOutput: {
1271
2065
  hookEventName: "PreToolUse",
1272
- additionalContext: [
1273
- "Lore-format git commit enforcement triggered.",
1274
- "- Add a blank line after the subject before the narrative body.",
1275
- "- Add a narrative body paragraph explaining the decision context.",
1276
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1277
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1278
- ].join("\n"),
1279
2066
  },
1280
2067
  systemMessage: [
1281
2068
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1306,13 +2093,6 @@ esac
1306
2093
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1307
2094
  hookSpecificOutput: {
1308
2095
  hookEventName: "PreToolUse",
1309
- additionalContext: [
1310
- "Lore-format git commit enforcement triggered.",
1311
- "- Add a blank line after the subject before the narrative body.",
1312
- "- Add a narrative body paragraph explaining the decision context.",
1313
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1314
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1315
- ].join("\n"),
1316
2096
  },
1317
2097
  systemMessage: [
1318
2098
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1343,13 +2123,6 @@ esac
1343
2123
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1344
2124
  hookSpecificOutput: {
1345
2125
  hookEventName: "PreToolUse",
1346
- additionalContext: [
1347
- "Lore-format git commit enforcement triggered.",
1348
- "- Add a blank line after the subject before the narrative body.",
1349
- "- Add a narrative body paragraph explaining the decision context.",
1350
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1351
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1352
- ].join("\n"),
1353
2126
  },
1354
2127
  systemMessage: [
1355
2128
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1380,13 +2153,6 @@ esac
1380
2153
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1381
2154
  hookSpecificOutput: {
1382
2155
  hookEventName: "PreToolUse",
1383
- additionalContext: [
1384
- "Lore-format git commit enforcement triggered.",
1385
- "- Add a blank line after the subject before the narrative body.",
1386
- "- Add a narrative body paragraph explaining the decision context.",
1387
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1388
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1389
- ].join("\n"),
1390
2156
  },
1391
2157
  systemMessage: [
1392
2158
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1417,13 +2183,6 @@ esac
1417
2183
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1418
2184
  hookSpecificOutput: {
1419
2185
  hookEventName: "PreToolUse",
1420
- additionalContext: [
1421
- "Lore-format git commit enforcement triggered.",
1422
- "- Add a blank line after the subject before the narrative body.",
1423
- "- Add a narrative body paragraph explaining the decision context.",
1424
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1425
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1426
- ].join("\n"),
1427
2186
  },
1428
2187
  systemMessage: [
1429
2188
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1454,13 +2213,6 @@ esac
1454
2213
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1455
2214
  hookSpecificOutput: {
1456
2215
  hookEventName: "PreToolUse",
1457
- additionalContext: [
1458
- "Lore-format git commit enforcement triggered.",
1459
- "- Add a blank line after the subject before the narrative body.",
1460
- "- Add a narrative body paragraph explaining the decision context.",
1461
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1462
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1463
- ].join("\n"),
1464
2216
  },
1465
2217
  systemMessage: [
1466
2218
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1491,13 +2243,6 @@ esac
1491
2243
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1492
2244
  hookSpecificOutput: {
1493
2245
  hookEventName: "PreToolUse",
1494
- additionalContext: [
1495
- "Lore-format git commit enforcement triggered.",
1496
- "- Add a blank line after the subject before the narrative body.",
1497
- "- Add a narrative body paragraph explaining the decision context.",
1498
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1499
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1500
- ].join("\n"),
1501
2246
  },
1502
2247
  systemMessage: [
1503
2248
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1528,13 +2273,6 @@ esac
1528
2273
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1529
2274
  hookSpecificOutput: {
1530
2275
  hookEventName: "PreToolUse",
1531
- additionalContext: [
1532
- "Lore-format git commit enforcement triggered.",
1533
- "- Add a blank line after the subject before the narrative body.",
1534
- "- Add a narrative body paragraph explaining the decision context.",
1535
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1536
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1537
- ].join("\n"),
1538
2276
  },
1539
2277
  systemMessage: [
1540
2278
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1565,10 +2303,6 @@ esac
1565
2303
  reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1566
2304
  hookSpecificOutput: {
1567
2305
  hookEventName: "PreToolUse",
1568
- additionalContext: [
1569
- "Lore-format git commit enforcement triggered.",
1570
- "- Use inline `git commit -m ...` paragraphs for Lore-format commits in this path; file/editor/reuse/fixup message sources are not inspectable safely from pre-tool-use enforcement.",
1571
- ].join("\n"),
1572
2306
  },
1573
2307
  systemMessage: [
1574
2308
  "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
@@ -1584,87 +2318,383 @@ esac
1584
2318
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-missing-omx-coauthor-"));
1585
2319
  try {
1586
2320
  const result = await dispatchCodexNativeHook({
1587
- hook_event_name: "PreToolUse",
2321
+ hook_event_name: "PreToolUse",
2322
+ cwd,
2323
+ tool_name: "Bash",
2324
+ tool_use_id: "tool-git-commit-missing-omx-coauthor",
2325
+ tool_input: {
2326
+ command: [
2327
+ 'git commit',
2328
+ '-m "Prevent invalid history from bypassing Lore enforcement"',
2329
+ '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
2330
+ '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
2331
+ '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
2332
+ ].join(" "),
2333
+ },
2334
+ }, { cwd });
2335
+ assert.equal(result.omxEventName, "pre-tool-use");
2336
+ assert.deepEqual(result.outputJson, {
2337
+ decision: "block",
2338
+ reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2339
+ hookSpecificOutput: {
2340
+ hookEventName: "PreToolUse",
2341
+ },
2342
+ systemMessage: [
2343
+ "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2344
+ "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2345
+ ].join("\n"),
2346
+ });
2347
+ }
2348
+ finally {
2349
+ await rm(cwd, { recursive: true, force: true });
2350
+ }
2351
+ });
2352
+ it("stays silent on PreToolUse for Lore-compliant git commit with OmX co-author trailer", async () => {
2353
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-valid-"));
2354
+ try {
2355
+ const result = await dispatchCodexNativeHook({
2356
+ hook_event_name: "PreToolUse",
2357
+ cwd,
2358
+ tool_name: "Bash",
2359
+ tool_use_id: "tool-git-commit-valid",
2360
+ tool_input: {
2361
+ command: [
2362
+ 'git commit',
2363
+ '-m "Prevent invalid history from bypassing Lore enforcement"',
2364
+ '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
2365
+ '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
2366
+ '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
2367
+ '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
2368
+ ].join(" "),
2369
+ },
2370
+ }, { cwd });
2371
+ assert.equal(result.omxEventName, "pre-tool-use");
2372
+ assert.equal(result.outputJson, null);
2373
+ }
2374
+ finally {
2375
+ await rm(cwd, { recursive: true, force: true });
2376
+ }
2377
+ });
2378
+ it("warns on PreToolUse git commit when mapped source changes lack staged docs refresh", async () => {
2379
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-warn-"));
2380
+ try {
2381
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
2382
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
2383
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
2384
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
2385
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
2386
+ await writeFile(join(cwd, "README.md"), "base\n", "utf-8");
2387
+ execFileSync("git", ["add", "README.md", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
2388
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
2389
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
2390
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
2391
+ const result = await dispatchCodexNativeHook({
2392
+ hook_event_name: "PreToolUse",
2393
+ cwd,
2394
+ tool_name: "Bash",
2395
+ tool_use_id: "tool-git-commit-doc-refresh-warn",
2396
+ tool_input: {
2397
+ command: [
2398
+ 'git commit',
2399
+ '-m "Keep native hooks aligned with docs"',
2400
+ '-m "Update the stop hook internals without refreshing the operator docs yet."',
2401
+ '-m "Constraint: native hook warning MVP must remain non-blocking on commit path"',
2402
+ '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
2403
+ '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
2404
+ ].join(" "),
2405
+ },
2406
+ }, { cwd });
2407
+ assert.equal(result.omxEventName, "pre-tool-use");
2408
+ assert.equal(result.outputJson?.decision, undefined);
2409
+ assert.equal(result.outputJson?.hookSpecificOutput?.hookEventName, "PreToolUse");
2410
+ assert.match(JSON.stringify(result.outputJson), /Document-refresh warning/);
2411
+ assert.match(JSON.stringify(result.outputJson), /docs\/codex-native-hooks\.md/);
2412
+ }
2413
+ finally {
2414
+ await rm(cwd, { recursive: true, force: true });
2415
+ }
2416
+ });
2417
+ it("does not warn on PreToolUse when relevant docs are staged", async () => {
2418
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-docs-"));
2419
+ try {
2420
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
2421
+ await mkdir(join(cwd, "docs"), { recursive: true });
2422
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
2423
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
2424
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
2425
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
2426
+ await writeFile(join(cwd, "docs", "codex-native-hooks.md"), "initial\n", "utf-8");
2427
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts", "docs/codex-native-hooks.md"], { cwd, stdio: "ignore" });
2428
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
2429
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
2430
+ await writeFile(join(cwd, "docs", "codex-native-hooks.md"), "updated\n", "utf-8");
2431
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts", "docs/codex-native-hooks.md"], { cwd, stdio: "ignore" });
2432
+ const result = await dispatchCodexNativeHook({
2433
+ hook_event_name: "PreToolUse",
2434
+ cwd,
2435
+ tool_name: "Bash",
2436
+ tool_use_id: "tool-git-commit-doc-refresh-docs",
2437
+ tool_input: {
2438
+ command: [
2439
+ 'git commit',
2440
+ '-m "Keep native hooks aligned with docs"',
2441
+ '-m "Update the stop hook internals and refresh the native hook docs together."',
2442
+ '-m "Constraint: native hook warning MVP must remain non-blocking on commit path"',
2443
+ '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
2444
+ '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
2445
+ ].join(" "),
2446
+ },
2447
+ }, { cwd });
2448
+ assert.equal(result.outputJson, null);
2449
+ }
2450
+ finally {
2451
+ await rm(cwd, { recursive: true, force: true });
2452
+ }
2453
+ });
2454
+ it("does not run commit-path document-refresh against payload cwd when git -C targets another repo", async () => {
2455
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-chdir-"));
2456
+ const otherRepo = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-other-"));
2457
+ try {
2458
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
2459
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
2460
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
2461
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
2462
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
2463
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
2464
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
2465
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
2466
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
2467
+ execFileSync("git", ["init"], { cwd: otherRepo, stdio: "ignore" });
2468
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: otherRepo, stdio: "ignore" });
2469
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: otherRepo, stdio: "ignore" });
2470
+ await writeFile(join(otherRepo, "README.md"), "base\n", "utf-8");
2471
+ execFileSync("git", ["add", "README.md"], { cwd: otherRepo, stdio: "ignore" });
2472
+ execFileSync("git", ["commit", "-m", "init"], { cwd: otherRepo, stdio: "ignore" });
2473
+ const result = await dispatchCodexNativeHook({
2474
+ hook_event_name: "PreToolUse",
2475
+ cwd,
2476
+ tool_name: "Bash",
2477
+ tool_use_id: "tool-git-commit-doc-refresh-chdir",
2478
+ tool_input: {
2479
+ command: [
2480
+ `git -C ${JSON.stringify(otherRepo)}`,
2481
+ 'commit',
2482
+ '-m "Keep native hooks aligned with docs"',
2483
+ '-m "Document-refresh check should not inspect the caller cwd when commit targets another repo."',
2484
+ '-m "Constraint: alternate git targets are skipped unless hook-side repo resolution is added explicitly"',
2485
+ '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
2486
+ '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
2487
+ ].join(" "),
2488
+ },
2489
+ }, { cwd });
2490
+ assert.equal(result.outputJson, null);
2491
+ }
2492
+ finally {
2493
+ await rm(cwd, { recursive: true, force: true });
2494
+ await rm(otherRepo, { recursive: true, force: true });
2495
+ }
2496
+ });
2497
+ it("suppresses PreToolUse document-refresh warning when commit message includes an exemption", async () => {
2498
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-exempt-"));
2499
+ try {
2500
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
2501
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
2502
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
2503
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
2504
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
2505
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
2506
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
2507
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
2508
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
2509
+ const result = await dispatchCodexNativeHook({
2510
+ hook_event_name: "PreToolUse",
2511
+ cwd,
2512
+ tool_name: "Bash",
2513
+ tool_use_id: "tool-git-commit-doc-refresh-exempt",
2514
+ tool_input: {
2515
+ command: [
2516
+ 'git commit',
2517
+ '-m "Keep native hooks aligned with docs"',
2518
+ '-m "Update the stop hook internals without docs refresh because behavior is internal-only."',
2519
+ '-m "Constraint: native hook warning MVP must remain non-blocking on commit path"',
2520
+ `-m "${DOCUMENT_REFRESH_EXEMPTION_PREFIX} internal-only behavior verified"`,
2521
+ '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
2522
+ '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
2523
+ ].join(" "),
2524
+ },
2525
+ }, { cwd });
2526
+ assert.equal(result.outputJson, null);
2527
+ }
2528
+ finally {
2529
+ await rm(cwd, { recursive: true, force: true });
2530
+ }
2531
+ });
2532
+ it("returns PostToolUse remediation guidance for command-not-found output", async () => {
2533
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-failure-"));
2534
+ try {
2535
+ const result = await dispatchCodexNativeHook({
2536
+ hook_event_name: "PostToolUse",
2537
+ cwd,
2538
+ tool_name: "Bash",
2539
+ tool_use_id: "tool-fail",
2540
+ tool_input: { command: "foo --version" },
2541
+ tool_response: "{\"exit_code\":127,\"stdout\":\"\",\"stderr\":\"bash: foo: command not found\"}",
2542
+ }, { cwd });
2543
+ assert.equal(result.omxEventName, "post-tool-use");
2544
+ assert.deepEqual(result.outputJson, {
2545
+ decision: "block",
2546
+ reason: "The Bash output indicates a command/setup failure that should be fixed before retrying.",
2547
+ hookSpecificOutput: {
2548
+ hookEventName: "PostToolUse",
2549
+ additionalContext: "Bash reported `command not found`, `permission denied`, or a missing file/path. Verify the command, dependency installation, PATH, file permissions, and referenced paths before retrying.",
2550
+ },
2551
+ });
2552
+ }
2553
+ finally {
2554
+ await rm(cwd, { recursive: true, force: true });
2555
+ }
2556
+ });
2557
+ it("stays silent when successful search output contains old Bash failure text", async () => {
2558
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-successful-search-"));
2559
+ try {
2560
+ const result = await dispatchCodexNativeHook({
2561
+ hook_event_name: "PostToolUse",
2562
+ cwd,
2563
+ tool_name: "Bash",
2564
+ tool_use_id: "tool-search-log",
2565
+ tool_input: { command: "rg 'command not found' .omx/logs" },
2566
+ tool_response: JSON.stringify({
2567
+ exit_code: 0,
2568
+ stdout: "old-session.log: bash: foo: command not found",
2569
+ stderr: "",
2570
+ }),
2571
+ }, { cwd });
2572
+ assert.equal(result.omxEventName, "post-tool-use");
2573
+ assert.equal(result.outputJson, null);
2574
+ }
2575
+ finally {
2576
+ await rm(cwd, { recursive: true, force: true });
2577
+ }
2578
+ });
2579
+ it("stays silent when Bash stdout only contains failure-like source text", async () => {
2580
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-failure-source-text-"));
2581
+ try {
2582
+ const result = await dispatchCodexNativeHook({
2583
+ hook_event_name: "PostToolUse",
2584
+ cwd,
2585
+ tool_name: "Bash",
2586
+ tool_use_id: "tool-source-text",
2587
+ tool_input: { command: "sed -n '1,40p' hook-source.ts" },
2588
+ tool_response: "const text = 'bash: foo: command not found';\nconst detail = 'permission denied';",
2589
+ }, { cwd });
2590
+ assert.equal(result.omxEventName, "post-tool-use");
2591
+ assert.equal(result.outputJson, null);
2592
+ }
2593
+ finally {
2594
+ await rm(cwd, { recursive: true, force: true });
2595
+ }
2596
+ });
2597
+ it("stays silent for rc-zero build logs that mention missing grep paths", async () => {
2598
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-build-log-"));
2599
+ try {
2600
+ const result = await dispatchCodexNativeHook({
2601
+ hook_event_name: "PostToolUse",
2602
+ cwd,
2603
+ tool_name: "Bash",
2604
+ tool_use_id: "tool-build-log",
2605
+ tool_input: { command: "npm run build" },
2606
+ tool_response: JSON.stringify({
2607
+ exit_code: 0,
2608
+ stdout: "build passed\nnote: grep fixture says no such file or directory",
2609
+ stderr: "",
2610
+ }),
2611
+ }, { cwd });
2612
+ assert.equal(result.omxEventName, "post-tool-use");
2613
+ assert.equal(result.outputJson, null);
2614
+ }
2615
+ finally {
2616
+ await rm(cwd, { recursive: true, force: true });
2617
+ }
2618
+ });
2619
+ it("does not treat Bash output containing MCP transport text as MCP transport death", async () => {
2620
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-source-text-"));
2621
+ try {
2622
+ const result = await dispatchCodexNativeHook({
2623
+ hook_event_name: "PostToolUse",
2624
+ cwd,
2625
+ tool_name: "Bash",
2626
+ tool_use_id: "tool-mcp-source-text",
2627
+ tool_input: { command: "sed -n '580,620p' codex-native-pre-post.ts" },
2628
+ tool_response: JSON.stringify({
2629
+ exit_code: 0,
2630
+ stdout: "reason: 'MCP transport closed before response over stdio pipe closed'",
2631
+ stderr: "",
2632
+ }),
2633
+ }, { cwd });
2634
+ assert.equal(result.omxEventName, "post-tool-use");
2635
+ assert.equal(result.outputJson, null);
2636
+ }
2637
+ finally {
2638
+ await rm(cwd, { recursive: true, force: true });
2639
+ }
2640
+ });
2641
+ it("stays silent when successful output includes prior hook context text", async () => {
2642
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-recursive-context-"));
2643
+ try {
2644
+ const result = await dispatchCodexNativeHook({
2645
+ hook_event_name: "PostToolUse",
1588
2646
  cwd,
1589
2647
  tool_name: "Bash",
1590
- tool_use_id: "tool-git-commit-missing-omx-coauthor",
1591
- tool_input: {
1592
- command: [
1593
- 'git commit',
1594
- '-m "Prevent invalid history from bypassing Lore enforcement"',
1595
- '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
1596
- '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
1597
- '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
1598
- ].join(" "),
1599
- },
2648
+ tool_use_id: "tool-hook-context",
2649
+ tool_input: { command: "cat transcript.txt" },
2650
+ tool_response: JSON.stringify({
2651
+ exit_code: 0,
2652
+ stdout: "Bash reported `command not found`, `permission denied`, or a missing file/path. Verify the command, dependency installation, PATH, file permissions, and referenced paths before retrying.",
2653
+ stderr: "",
2654
+ }),
1600
2655
  }, { cwd });
1601
- assert.equal(result.omxEventName, "pre-tool-use");
1602
- assert.deepEqual(result.outputJson, {
1603
- decision: "block",
1604
- reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1605
- hookSpecificOutput: {
1606
- hookEventName: "PreToolUse",
1607
- additionalContext: [
1608
- "Lore-format git commit enforcement triggered.",
1609
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1610
- ].join("\n"),
1611
- },
1612
- systemMessage: [
1613
- "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1614
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1615
- ].join("\n"),
1616
- });
2656
+ assert.equal(result.omxEventName, "post-tool-use");
2657
+ assert.equal(result.outputJson, null);
1617
2658
  }
1618
2659
  finally {
1619
2660
  await rm(cwd, { recursive: true, force: true });
1620
2661
  }
1621
2662
  });
1622
- it("stays silent on PreToolUse for Lore-compliant git commit with OmX co-author trailer", async () => {
1623
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-valid-"));
2663
+ it("stays silent when successful Bash output quotes MCP transport warnings", async () => {
2664
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-bash-mcp-quote-"));
1624
2665
  try {
1625
2666
  const result = await dispatchCodexNativeHook({
1626
- hook_event_name: "PreToolUse",
2667
+ hook_event_name: "PostToolUse",
1627
2668
  cwd,
1628
2669
  tool_name: "Bash",
1629
- tool_use_id: "tool-git-commit-valid",
1630
- tool_input: {
1631
- command: [
1632
- 'git commit',
1633
- '-m "Prevent invalid history from bypassing Lore enforcement"',
1634
- '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
1635
- '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
1636
- '-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
1637
- '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
1638
- ].join(" "),
1639
- },
2670
+ tool_use_id: "tool-bash-mcp-quote",
2671
+ tool_input: { command: "cat diagnostic-log.txt" },
2672
+ tool_response: JSON.stringify({
2673
+ exit_code: 0,
2674
+ stdout: "diagnostic log quoted: MCP transport closed; stdio pipe closed before response",
2675
+ stderr: "",
2676
+ }),
1640
2677
  }, { cwd });
1641
- assert.equal(result.omxEventName, "pre-tool-use");
2678
+ assert.equal(result.omxEventName, "post-tool-use");
1642
2679
  assert.equal(result.outputJson, null);
1643
2680
  }
1644
2681
  finally {
1645
2682
  await rm(cwd, { recursive: true, force: true });
1646
2683
  }
1647
2684
  });
1648
- it("returns PostToolUse remediation guidance for command-not-found output", async () => {
1649
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-failure-"));
2685
+ it("stays silent when Bash hard-failure text has no parsed exit code", async () => {
2686
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-bash-unparsed-failure-"));
1650
2687
  try {
1651
2688
  const result = await dispatchCodexNativeHook({
1652
2689
  hook_event_name: "PostToolUse",
1653
2690
  cwd,
1654
2691
  tool_name: "Bash",
1655
- tool_use_id: "tool-fail",
1656
- tool_input: { command: "foo --version" },
1657
- tool_response: "{\"exit_code\":127,\"stdout\":\"\",\"stderr\":\"bash: foo: command not found\"}",
2692
+ tool_use_id: "tool-bash-unparsed-failure",
2693
+ tool_input: { command: "cat captured-output.txt" },
2694
+ tool_response: "captured transcript says: bash: foo: command not found",
1658
2695
  }, { cwd });
1659
2696
  assert.equal(result.omxEventName, "post-tool-use");
1660
- assert.deepEqual(result.outputJson, {
1661
- decision: "block",
1662
- reason: "The Bash output indicates a command/setup failure that should be fixed before retrying.",
1663
- hookSpecificOutput: {
1664
- hookEventName: "PostToolUse",
1665
- additionalContext: "Bash reported `command not found`, `permission denied`, or a missing file/path. Verify the command, dependency installation, PATH, file permissions, and referenced paths before retrying.",
1666
- },
1667
- });
2697
+ assert.equal(result.outputJson, null);
1668
2698
  }
1669
2699
  finally {
1670
2700
  await rm(cwd, { recursive: true, force: true });
@@ -2116,7 +3146,17 @@ esac
2116
3146
  const prevLeaderCwd = process.env.OMX_TEAM_LEADER_CWD;
2117
3147
  try {
2118
3148
  await initTeamState("worker-stop-team", "worker stop fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker" });
3149
+ const workerCwd = join(cwd, ".omx", "team", "worker-stop-team", "worktrees", "worker-1");
2119
3150
  const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team", "workers", "worker-1");
3151
+ await mkdir(workerCwd, { recursive: true });
3152
+ await writeJson(join(workerDir, "identity.json"), {
3153
+ name: "worker-1",
3154
+ index: 1,
3155
+ role: "executor",
3156
+ assigned_tasks: ["1"],
3157
+ worktree_path: workerCwd,
3158
+ team_state_root: join(cwd, ".omx", "state"),
3159
+ });
2120
3160
  await writeJson(join(workerDir, "status.json"), {
2121
3161
  state: "idle",
2122
3162
  current_task_id: "1",
@@ -2135,9 +3175,9 @@ esac
2135
3175
  process.env.OMX_TEAM_LEADER_CWD = cwd;
2136
3176
  const result = await dispatchCodexNativeHook({
2137
3177
  hook_event_name: "Stop",
2138
- cwd: join(cwd, ".omx", "team", "worker-stop-team", "worktrees", "worker-1"),
3178
+ cwd: workerCwd,
2139
3179
  session_id: "sess-stop-team-worker",
2140
- }, { cwd: join(cwd, ".omx", "team", "worker-stop-team", "worktrees", "worker-1") });
3180
+ }, { cwd: workerCwd });
2141
3181
  assert.deepEqual(result.outputJson, {
2142
3182
  decision: "block",
2143
3183
  reason: "OMX team worker worker-1 is still assigned non-terminal task 1 (in_progress); continue the current assigned task or report a concrete blocker before stopping.",
@@ -2161,6 +3201,79 @@ esac
2161
3201
  await rm(cwd, { recursive: true, force: true });
2162
3202
  }
2163
3203
  });
3204
+ it("suppresses identical team worker Stop replays but re-blocks fresh turns and task state changes", async () => {
3205
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
3206
+ try {
3207
+ await initTeamState("worker-repeat-team", "worker stop repeat guard", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-repeat" });
3208
+ const stateDir = join(cwd, ".omx", "state");
3209
+ const workerDir = join(stateDir, "team", "worker-repeat-team", "workers", "worker-1");
3210
+ const taskPath = join(stateDir, "team", "worker-repeat-team", "tasks", "task-1.json");
3211
+ const workerCwd = join(cwd, ".omx", "team", "worker-repeat-team", "worktrees", "worker-1");
3212
+ await mkdir(workerCwd, { recursive: true });
3213
+ await writeJson(join(workerDir, "identity.json"), {
3214
+ name: "worker-1",
3215
+ index: 1,
3216
+ role: "executor",
3217
+ assigned_tasks: ["1"],
3218
+ worktree_path: workerCwd,
3219
+ team_state_root: stateDir,
3220
+ });
3221
+ await writeJson(join(workerDir, "status.json"), {
3222
+ state: "idle",
3223
+ current_task_id: "1",
3224
+ updated_at: new Date().toISOString(),
3225
+ });
3226
+ await writeJson(taskPath, {
3227
+ id: "1",
3228
+ subject: "hook task",
3229
+ description: "finish hook task",
3230
+ status: "in_progress",
3231
+ owner: "worker-1",
3232
+ created_at: new Date().toISOString(),
3233
+ });
3234
+ process.env.OMX_TEAM_WORKER = "worker-repeat-team/worker-1";
3235
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
3236
+ process.env.OMX_TEAM_LEADER_CWD = cwd;
3237
+ const basePayload = {
3238
+ hook_event_name: "Stop",
3239
+ cwd: workerCwd,
3240
+ session_id: "sess-stop-team-worker-repeat",
3241
+ thread_id: "thread-stop-team-worker-repeat",
3242
+ turn_id: "turn-stop-team-worker-repeat-1",
3243
+ last_assistant_message: "I need to stop before this task is done.",
3244
+ };
3245
+ const expectedInProgress = {
3246
+ decision: "block",
3247
+ reason: "OMX team worker worker-1 is still assigned non-terminal task 1 (in_progress); continue the current assigned task or report a concrete blocker before stopping.",
3248
+ stopReason: "team_worker_worker-1_1_in_progress",
3249
+ systemMessage: "OMX team worker worker-1 is still assigned task 1 (in_progress).",
3250
+ };
3251
+ const first = await dispatchCodexNativeHook(basePayload, { cwd: workerCwd });
3252
+ const replay = await dispatchCodexNativeHook({ ...basePayload, stop_hook_active: true }, { cwd: workerCwd });
3253
+ const freshTurn = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true }, { cwd: workerCwd });
3254
+ await writeJson(taskPath, {
3255
+ id: "1",
3256
+ subject: "hook task",
3257
+ description: "finish hook task",
3258
+ status: "blocked",
3259
+ owner: "worker-1",
3260
+ created_at: new Date().toISOString(),
3261
+ });
3262
+ const stateChanged = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true }, { cwd: workerCwd });
3263
+ assert.deepEqual(first.outputJson, expectedInProgress);
3264
+ assert.deepEqual(replay.outputJson, null);
3265
+ assert.deepEqual(freshTurn.outputJson, expectedInProgress);
3266
+ assert.deepEqual(stateChanged.outputJson, {
3267
+ decision: "block",
3268
+ reason: "OMX team worker worker-1 is still assigned non-terminal task 1 (blocked); continue the current assigned task or report a concrete blocker before stopping.",
3269
+ stopReason: "team_worker_worker-1_1_blocked",
3270
+ systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
3271
+ });
3272
+ }
3273
+ finally {
3274
+ await rm(cwd, { recursive: true, force: true });
3275
+ }
3276
+ });
2164
3277
  it("does not block Stop for a team worker when assigned task is terminal", async () => {
2165
3278
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
2166
3279
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
@@ -2280,6 +3393,38 @@ esac
2280
3393
  await rm(cwd, { recursive: true, force: true });
2281
3394
  }
2282
3395
  });
3396
+ it("honors terminal team run-state before later canonical-team Stop fallback", async () => {
3397
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-terminal-run-state-canonical-"));
3398
+ try {
3399
+ const stateDir = join(cwd, ".omx", "state");
3400
+ const sessionId = "sess-stop-team-terminal-run-state";
3401
+ await initTeamState("terminal-run-state-team", "terminal team stop canonical fallback regression", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: sessionId });
3402
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
3403
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
3404
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
3405
+ version: 1,
3406
+ mode: "team",
3407
+ active: false,
3408
+ outcome: "finish",
3409
+ lifecycle_outcome: "finished",
3410
+ current_phase: "complete",
3411
+ completed_at: "2026-04-27T12:00:00.000Z",
3412
+ updated_at: "2026-04-27T12:00:00.000Z",
3413
+ });
3414
+ const result = await dispatchCodexNativeHook({
3415
+ hook_event_name: "Stop",
3416
+ cwd,
3417
+ session_id: sessionId,
3418
+ thread_id: "thread-stop-team-terminal-run-state",
3419
+ turn_id: "turn-stop-team-terminal-run-state-1",
3420
+ }, { cwd });
3421
+ assert.equal(result.omxEventName, "stop");
3422
+ assert.equal(result.outputJson, null);
3423
+ }
3424
+ finally {
3425
+ await rm(cwd, { recursive: true, force: true });
3426
+ }
3427
+ });
2283
3428
  it("re-fires canonical-team Stop output for a later fresh Stop reply when coarse mode state is missing", async () => {
2284
3429
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-canonical-refire-"));
2285
3430
  try {
@@ -3072,6 +4217,105 @@ esac
3072
4217
  await rm(cwd, { recursive: true, force: true });
3073
4218
  }
3074
4219
  });
4220
+ it("returns a non-blocking Stop document-refresh warning before auto-nudge when Ralph is not active", async () => {
4221
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-"));
4222
+ try {
4223
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
4224
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
4225
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
4226
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
4227
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
4228
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
4229
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
4230
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
4231
+ const result = await dispatchCodexNativeHook({
4232
+ hook_event_name: "Stop",
4233
+ cwd,
4234
+ session_id: "sess-stop-doc-refresh",
4235
+ last_assistant_message: "Launch-ready: yes",
4236
+ }, { cwd });
4237
+ assert.equal(result.omxEventName, "stop");
4238
+ assert.equal(result.outputJson?.decision, undefined);
4239
+ assert.equal(result.outputJson?.hookSpecificOutput?.hookEventName, "Stop");
4240
+ assert.match(JSON.stringify(result.outputJson), /Document-refresh warning/);
4241
+ assert.match(JSON.stringify(result.outputJson), /staged \+ unstaged changes/);
4242
+ }
4243
+ finally {
4244
+ await rm(cwd, { recursive: true, force: true });
4245
+ }
4246
+ });
4247
+ it("does not warn on ordinary non-terminal Stop attempts before auto-nudge", async () => {
4248
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-nonterminal-"));
4249
+ try {
4250
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
4251
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
4252
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
4253
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
4254
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
4255
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
4256
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
4257
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
4258
+ const result = await dispatchCodexNativeHook({
4259
+ hook_event_name: "Stop",
4260
+ cwd,
4261
+ session_id: "sess-stop-doc-refresh-nonterminal",
4262
+ last_assistant_message: "Continuing implementation; next I will run focused tests.",
4263
+ }, { cwd });
4264
+ assert.equal(result.outputJson, null);
4265
+ }
4266
+ finally {
4267
+ await rm(cwd, { recursive: true, force: true });
4268
+ }
4269
+ });
4270
+ it("dedupes identical Stop document-refresh warnings during active Stop-hook replays", async () => {
4271
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-dedupe-"));
4272
+ try {
4273
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
4274
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
4275
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
4276
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
4277
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
4278
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
4279
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
4280
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
4281
+ const payload = {
4282
+ hook_event_name: "Stop",
4283
+ cwd,
4284
+ session_id: "sess-stop-doc-refresh-dedupe",
4285
+ last_assistant_message: "Launch-ready: yes",
4286
+ };
4287
+ const first = await dispatchCodexNativeHook(payload, { cwd });
4288
+ const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
4289
+ assert.match(JSON.stringify(first.outputJson), /Document-refresh warning/);
4290
+ assert.equal(replay.outputJson, null);
4291
+ }
4292
+ finally {
4293
+ await rm(cwd, { recursive: true, force: true });
4294
+ }
4295
+ });
4296
+ it("suppresses Stop document-refresh warning when the final handoff message includes an exemption", async () => {
4297
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-exempt-"));
4298
+ try {
4299
+ await mkdir(join(cwd, "src", "scripts"), { recursive: true });
4300
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
4301
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
4302
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
4303
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
4304
+ execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
4305
+ execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
4306
+ await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
4307
+ const result = await dispatchCodexNativeHook({
4308
+ hook_event_name: "Stop",
4309
+ cwd,
4310
+ session_id: "sess-stop-doc-refresh-exempt",
4311
+ last_assistant_message: `${DOCUMENT_REFRESH_EXEMPTION_PREFIX} internal-only behavior verified`,
4312
+ }, { cwd });
4313
+ assert.equal(result.outputJson, null);
4314
+ }
4315
+ finally {
4316
+ await rm(cwd, { recursive: true, force: true });
4317
+ }
4318
+ });
3075
4319
  it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
3076
4320
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
3077
4321
  try {
@@ -3088,9 +4332,9 @@ esac
3088
4332
  assert.equal(result.omxEventName, "stop");
3089
4333
  assert.deepEqual(result.outputJson, {
3090
4334
  decision: "block",
3091
- reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
4335
+ reason: "OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
3092
4336
  stopReason: "ralph_executing",
3093
- systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
4337
+ systemMessage: "OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
3094
4338
  });
3095
4339
  }
3096
4340
  finally {
@@ -3116,9 +4360,9 @@ esac
3116
4360
  assert.equal(result.omxEventName, "stop");
3117
4361
  assert.deepEqual(result.outputJson, {
3118
4362
  decision: "block",
3119
- reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
4363
+ reason: "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-live-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
3120
4364
  stopReason: "ralph_executing",
3121
- systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
4365
+ systemMessage: "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-live-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
3122
4366
  });
3123
4367
  }
3124
4368
  finally {
@@ -3143,48 +4387,138 @@ esac
3143
4387
  session_id: "sess-current",
3144
4388
  }, { cwd });
3145
4389
  assert.equal(result.omxEventName, "stop");
3146
- assert.equal(result.outputJson, null);
4390
+ assert.equal(result.outputJson, null);
4391
+ }
4392
+ finally {
4393
+ await rm(cwd, { recursive: true, force: true });
4394
+ }
4395
+ });
4396
+ it("does not block Stop from stale current-session Ralph state when session.json points to a dead owner", async () => {
4397
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-current-session-ralph-"));
4398
+ try {
4399
+ const stateDir = join(cwd, ".omx", "state");
4400
+ await mkdir(join(stateDir, "sessions", "sess-dead"), { recursive: true });
4401
+ await writeJson(join(stateDir, "session.json"), {
4402
+ session_id: "sess-dead",
4403
+ cwd,
4404
+ pid: Number.MAX_SAFE_INTEGER,
4405
+ started_at: "2026-01-01T00:00:00.000Z",
4406
+ });
4407
+ await writeJson(join(stateDir, "sessions", "sess-dead", "ralph-state.json"), {
4408
+ active: true,
4409
+ current_phase: "verifying",
4410
+ session_id: "sess-dead",
4411
+ });
4412
+ await writeJson(join(stateDir, "skill-active-state.json"), {
4413
+ active: true,
4414
+ skill: "team",
4415
+ phase: "team-exec",
4416
+ active_skills: [{ skill: "team", phase: "team-exec", active: true, session_id: "sess-dead" }],
4417
+ });
4418
+ await writeJson(join(stateDir, "native-stop-state.json"), {
4419
+ sessions: {
4420
+ "sess-dead": {
4421
+ last_signature: "ralph-stop|sess-dead|thread-1|no-message|verifying",
4422
+ updated_at: "2026-04-20T21:00:00.000Z",
4423
+ },
4424
+ },
4425
+ });
4426
+ const result = await dispatchCodexNativeHook({
4427
+ hook_event_name: "Stop",
4428
+ cwd,
4429
+ session_id: "sess-dead",
4430
+ thread_id: "thread-1",
4431
+ stop_hook_active: true,
4432
+ }, { cwd });
4433
+ assert.equal(result.omxEventName, "stop");
4434
+ assert.equal(result.outputJson, null);
4435
+ }
4436
+ finally {
4437
+ await rm(cwd, { recursive: true, force: true });
4438
+ }
4439
+ });
4440
+ it("does not hard-block Stop on stale session-scoped Ralph starting state after visible active modes are cleared", async () => {
4441
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-cleared-stale-ralph-"));
4442
+ try {
4443
+ const stateDir = join(cwd, ".omx", "state");
4444
+ const sessionId = "sess-cleared-ralph";
4445
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
4446
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
4447
+ active: true,
4448
+ mode: "ralph",
4449
+ current_phase: "starting",
4450
+ session_id: sessionId,
4451
+ });
4452
+ await writeJson(join(stateDir, "skill-active-state.json"), {
4453
+ active: false,
4454
+ skill: "ralph",
4455
+ active_skills: [],
4456
+ });
4457
+ const listActive = await executeStateOperation("state_list_active", {
4458
+ workingDirectory: cwd,
4459
+ });
4460
+ assert.deepEqual(listActive.payload, { active_modes: [] });
4461
+ const result = await dispatchCodexNativeHook({
4462
+ hook_event_name: "Stop",
4463
+ cwd,
4464
+ session_id: sessionId,
4465
+ }, { cwd });
4466
+ assert.equal(result.omxEventName, "stop");
4467
+ assert.equal(result.outputJson, null);
4468
+ }
4469
+ finally {
4470
+ await rm(cwd, { recursive: true, force: true });
4471
+ }
4472
+ });
4473
+ it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
4474
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
4475
+ try {
4476
+ const stateDir = join(cwd, ".omx", "state");
4477
+ const sessionId = "sess-visible-ralph";
4478
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
4479
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
4480
+ active: true,
4481
+ mode: "ralph",
4482
+ current_phase: "starting",
4483
+ session_id: sessionId,
4484
+ });
4485
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
4486
+ active: true,
4487
+ skill: "ralph",
4488
+ phase: "starting",
4489
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
4490
+ });
4491
+ const result = await dispatchCodexNativeHook({
4492
+ hook_event_name: "Stop",
4493
+ cwd,
4494
+ session_id: sessionId,
4495
+ }, { cwd });
4496
+ assert.equal(result.omxEventName, "stop");
4497
+ assert.deepEqual(result.outputJson, {
4498
+ decision: "block",
4499
+ reason: "OMX Ralph is still active (phase: starting; state: .omx/state/sessions/sess-visible-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
4500
+ stopReason: "ralph_starting",
4501
+ systemMessage: "OMX Ralph is still active (phase: starting; state: .omx/state/sessions/sess-visible-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
4502
+ });
3147
4503
  }
3148
4504
  finally {
3149
4505
  await rm(cwd, { recursive: true, force: true });
3150
4506
  }
3151
4507
  });
3152
- it("does not block Stop from stale current-session Ralph state when session.json points to a dead owner", async () => {
3153
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-current-session-ralph-"));
4508
+ it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
4509
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
3154
4510
  try {
3155
4511
  const stateDir = join(cwd, ".omx", "state");
3156
- await mkdir(join(stateDir, "sessions", "sess-dead"), { recursive: true });
3157
- await writeJson(join(stateDir, "session.json"), {
3158
- session_id: "sess-dead",
3159
- cwd,
3160
- pid: Number.MAX_SAFE_INTEGER,
3161
- started_at: "2026-01-01T00:00:00.000Z",
3162
- });
3163
- await writeJson(join(stateDir, "sessions", "sess-dead", "ralph-state.json"), {
3164
- active: true,
3165
- current_phase: "verifying",
3166
- session_id: "sess-dead",
3167
- });
3168
- await writeJson(join(stateDir, "skill-active-state.json"), {
4512
+ await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
4513
+ await writeJson(join(stateDir, "sessions", "sess-other", "ralph-state.json"), {
3169
4514
  active: true,
3170
- skill: "team",
3171
- phase: "team-exec",
3172
- active_skills: [{ skill: "team", phase: "team-exec", active: true, session_id: "sess-dead" }],
3173
- });
3174
- await writeJson(join(stateDir, "native-stop-state.json"), {
3175
- sessions: {
3176
- "sess-dead": {
3177
- last_signature: "ralph-stop|sess-dead|thread-1|no-message|verifying",
3178
- updated_at: "2026-04-20T21:00:00.000Z",
3179
- },
3180
- },
4515
+ current_phase: "starting",
4516
+ session_id: "sess-other",
3181
4517
  });
3182
4518
  const result = await dispatchCodexNativeHook({
3183
4519
  hook_event_name: "Stop",
3184
4520
  cwd,
3185
- session_id: "sess-dead",
3186
- thread_id: "thread-1",
3187
- stop_hook_active: true,
4521
+ session_id: "sess-current",
3188
4522
  }, { cwd });
3189
4523
  assert.equal(result.omxEventName, "stop");
3190
4524
  assert.equal(result.outputJson, null);
@@ -3193,20 +4527,33 @@ esac
3193
4527
  await rm(cwd, { recursive: true, force: true });
3194
4528
  }
3195
4529
  });
3196
- it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
3197
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
4530
+ it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
4531
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
3198
4532
  try {
3199
4533
  const stateDir = join(cwd, ".omx", "state");
3200
- await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
3201
- await writeJson(join(stateDir, "sessions", "sess-other", "ralph-state.json"), {
4534
+ const sessionId = "sess-canonical-run-state-ralph";
4535
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
4536
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
4537
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
4538
+ version: 1,
4539
+ mode: "ralph",
4540
+ active: false,
4541
+ outcome: "finish",
4542
+ lifecycle_outcome: "finished",
4543
+ current_phase: "complete",
4544
+ completed_at: "2026-04-27T12:00:00.000Z",
4545
+ updated_at: "2026-04-27T12:00:00.000Z",
4546
+ });
4547
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
3202
4548
  active: true,
3203
- current_phase: "starting",
3204
- session_id: "sess-other",
4549
+ mode: "ralph",
4550
+ current_phase: "verifying",
4551
+ session_id: sessionId,
3205
4552
  });
3206
4553
  const result = await dispatchCodexNativeHook({
3207
4554
  hook_event_name: "Stop",
3208
4555
  cwd,
3209
- session_id: "sess-current",
4556
+ session_id: sessionId,
3210
4557
  }, { cwd });
3211
4558
  assert.equal(result.omxEventName, "stop");
3212
4559
  assert.equal(result.outputJson, null);
@@ -3308,9 +4655,9 @@ esac
3308
4655
  };
3309
4656
  const expected = {
3310
4657
  decision: "block",
3311
- reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
4658
+ reason: "OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
3312
4659
  stopReason: "ralph_executing",
3313
- systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
4660
+ systemMessage: "OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
3314
4661
  };
3315
4662
  const first = await dispatchCodexNativeHook(payload, { cwd });
3316
4663
  const replay = await dispatchCodexNativeHook({
@@ -4421,6 +5768,333 @@ describe("codex native hook triage integration", () => {
4421
5768
  await rm(cwd, { recursive: true, force: true });
4422
5769
  }
4423
5770
  });
5771
+ it("injects LIGHT/researcher advisory and writes state for an official-doc lookup prompt", async () => {
5772
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-researcher-"));
5773
+ try {
5774
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5775
+ const result = await dispatchCodexNativeHook({
5776
+ hook_event_name: "UserPromptSubmit",
5777
+ cwd,
5778
+ session_id: "triage-researcher-1",
5779
+ thread_id: "thread-triage-researcher-1",
5780
+ turn_id: "turn-triage-researcher-1",
5781
+ prompt: "Find the official docs and version compatibility notes for this SDK",
5782
+ }, { cwd });
5783
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5784
+ assert.match(additionalContext, /external documentation\/reference research request/);
5785
+ assert.match(additionalContext, /Prefer the researcher role surface/);
5786
+ assert.doesNotMatch(additionalContext, /skill: researcher activated/);
5787
+ assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
5788
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-1", "prompt-routing-state.json");
5789
+ assert.equal(existsSync(stateFile), true);
5790
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5791
+ assert.equal(state.last_triage?.lane, "LIGHT");
5792
+ assert.equal(state.last_triage?.destination, "researcher");
5793
+ assert.equal(state.last_triage?.reason, "external_reference_research");
5794
+ assert.equal(state.suppress_followup, true);
5795
+ }
5796
+ finally {
5797
+ await rm(cwd, { recursive: true, force: true });
5798
+ }
5799
+ });
5800
+ it("routes Korean external lookup phrasing to researcher without treating it as workflow activation", async () => {
5801
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-researcher-ko-"));
5802
+ try {
5803
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5804
+ const result = await dispatchCodexNativeHook({
5805
+ hook_event_name: "UserPromptSubmit",
5806
+ cwd,
5807
+ session_id: "triage-researcher-ko-1",
5808
+ thread_id: "thread-triage-researcher-ko-1",
5809
+ turn_id: "turn-triage-researcher-ko-1",
5810
+ prompt: "OpenAI Responses API 공식 문서 찾아줘",
5811
+ }, { cwd });
5812
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5813
+ assert.match(additionalContext, /Prefer the researcher role surface/);
5814
+ assert.equal(result.skillState, null);
5815
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-ko-1", "prompt-routing-state.json");
5816
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5817
+ assert.equal(state.last_triage?.lane, "LIGHT");
5818
+ assert.equal(state.last_triage?.destination, "researcher");
5819
+ }
5820
+ finally {
5821
+ await rm(cwd, { recursive: true, force: true });
5822
+ }
5823
+ });
5824
+ it("routes official-doc question prompts to researcher instead of explore", async () => {
5825
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-question-researcher-"));
5826
+ try {
5827
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5828
+ const result = await dispatchCodexNativeHook({
5829
+ hook_event_name: "UserPromptSubmit",
5830
+ cwd,
5831
+ session_id: "triage-question-researcher-1",
5832
+ thread_id: "thread-triage-question-researcher-1",
5833
+ turn_id: "turn-triage-question-researcher-1",
5834
+ prompt: "where can I find official docs for OpenAI Responses API?",
5835
+ }, { cwd });
5836
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5837
+ assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
5838
+ assert.match(additionalContext, /Prefer the researcher role surface/);
5839
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-question-researcher-1", "prompt-routing-state.json");
5840
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5841
+ assert.equal(state.last_triage?.lane, "LIGHT");
5842
+ assert.equal(state.last_triage?.destination, "researcher");
5843
+ assert.equal(state.last_triage?.reason, "external_reference_research");
5844
+ }
5845
+ finally {
5846
+ await rm(cwd, { recursive: true, force: true });
5847
+ }
5848
+ });
5849
+ it("routes endpoint-shaped official-doc lookups to researcher instead of local explore", async () => {
5850
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-endpoint-researcher-"));
5851
+ try {
5852
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5853
+ const result = await dispatchCodexNativeHook({
5854
+ hook_event_name: "UserPromptSubmit",
5855
+ cwd,
5856
+ session_id: "triage-endpoint-researcher-1",
5857
+ thread_id: "thread-triage-endpoint-researcher-1",
5858
+ turn_id: "turn-triage-endpoint-researcher-1",
5859
+ prompt: "find official docs for api/v1/responses",
5860
+ }, { cwd });
5861
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5862
+ assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
5863
+ assert.match(additionalContext, /Prefer the researcher role surface/);
5864
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-endpoint-researcher-1", "prompt-routing-state.json");
5865
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5866
+ assert.equal(state.last_triage?.lane, "LIGHT");
5867
+ assert.equal(state.last_triage?.destination, "researcher");
5868
+ assert.equal(state.last_triage?.reason, "external_reference_research");
5869
+ }
5870
+ finally {
5871
+ await rm(cwd, { recursive: true, force: true });
5872
+ }
5873
+ });
5874
+ it("routes dotted technology official-doc lookups to researcher instead of local explore", async () => {
5875
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-dotted-tech-researcher-"));
5876
+ try {
5877
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5878
+ const result = await dispatchCodexNativeHook({
5879
+ hook_event_name: "UserPromptSubmit",
5880
+ cwd,
5881
+ session_id: "triage-dotted-tech-researcher-1",
5882
+ thread_id: "thread-triage-dotted-tech-researcher-1",
5883
+ turn_id: "turn-triage-dotted-tech-researcher-1",
5884
+ prompt: "find official docs for Node.js",
5885
+ }, { cwd });
5886
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5887
+ assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
5888
+ assert.match(additionalContext, /Prefer the researcher role surface/);
5889
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-dotted-tech-researcher-1", "prompt-routing-state.json");
5890
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5891
+ assert.equal(state.last_triage?.lane, "LIGHT");
5892
+ assert.equal(state.last_triage?.destination, "researcher");
5893
+ assert.equal(state.last_triage?.reason, "external_reference_research");
5894
+ }
5895
+ finally {
5896
+ await rm(cwd, { recursive: true, force: true });
5897
+ }
5898
+ });
5899
+ it("routes URL-shaped official-doc lookups with repo paths to researcher instead of local routes", async () => {
5900
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-url-path-researcher-"));
5901
+ try {
5902
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5903
+ const result = await dispatchCodexNativeHook({
5904
+ hook_event_name: "UserPromptSubmit",
5905
+ cwd,
5906
+ session_id: "triage-url-path-researcher-1",
5907
+ thread_id: "thread-triage-url-path-researcher-1",
5908
+ turn_id: "turn-triage-url-path-researcher-1",
5909
+ prompt: "find official docs for github.com/org/repo/src/foo.ts",
5910
+ }, { cwd });
5911
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5912
+ assert.doesNotMatch(additionalContext, /Prefer the executor role surface/);
5913
+ assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
5914
+ assert.match(additionalContext, /Prefer the researcher role surface/);
5915
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-url-path-researcher-1", "prompt-routing-state.json");
5916
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5917
+ assert.equal(state.last_triage?.lane, "LIGHT");
5918
+ assert.equal(state.last_triage?.destination, "researcher");
5919
+ assert.equal(state.last_triage?.reason, "external_reference_research");
5920
+ }
5921
+ finally {
5922
+ await rm(cwd, { recursive: true, force: true });
5923
+ }
5924
+ });
5925
+ it("keeps implementation-shaped official-doc prompts on HEAVY instead of researcher", async () => {
5926
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-researcher-implementation-"));
5927
+ try {
5928
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5929
+ const result = await dispatchCodexNativeHook({
5930
+ hook_event_name: "UserPromptSubmit",
5931
+ cwd,
5932
+ session_id: "triage-researcher-implementation-1",
5933
+ thread_id: "thread-triage-researcher-implementation-1",
5934
+ turn_id: "turn-triage-researcher-implementation-1",
5935
+ prompt: "implement auth using official docs for the SDK",
5936
+ }, { cwd });
5937
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5938
+ assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
5939
+ assert.match(additionalContext, /multi-step goal with no workflow keyword/);
5940
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-implementation-1", "prompt-routing-state.json");
5941
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5942
+ assert.equal(state.last_triage?.lane, "HEAVY");
5943
+ assert.equal(state.last_triage?.destination, "autopilot");
5944
+ assert.equal(state.last_triage?.reason, "implementation_research_goal");
5945
+ }
5946
+ finally {
5947
+ await rm(cwd, { recursive: true, force: true });
5948
+ }
5949
+ });
5950
+ it("keeps planning-shaped official-doc prompts on HEAVY instead of researcher", async () => {
5951
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-researcher-planning-"));
5952
+ try {
5953
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5954
+ const result = await dispatchCodexNativeHook({
5955
+ hook_event_name: "UserPromptSubmit",
5956
+ cwd,
5957
+ session_id: "triage-researcher-planning-1",
5958
+ thread_id: "thread-triage-researcher-planning-1",
5959
+ turn_id: "turn-triage-researcher-planning-1",
5960
+ prompt: "research and plan auth migration using official docs for the SDK",
5961
+ }, { cwd });
5962
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5963
+ assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
5964
+ assert.match(additionalContext, /multi-step goal with no workflow keyword/);
5965
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-planning-1", "prompt-routing-state.json");
5966
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5967
+ assert.equal(state.last_triage?.lane, "HEAVY");
5968
+ assert.equal(state.last_triage?.destination, "autopilot");
5969
+ assert.equal(state.last_triage?.reason, "implementation_research_goal");
5970
+ }
5971
+ finally {
5972
+ await rm(cwd, { recursive: true, force: true });
5973
+ }
5974
+ });
5975
+ it("keeps local source lookup prompts off researcher", async () => {
5976
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-local-source-explore-"));
5977
+ try {
5978
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
5979
+ const result = await dispatchCodexNativeHook({
5980
+ hook_event_name: "UserPromptSubmit",
5981
+ cwd,
5982
+ session_id: "triage-local-source-1",
5983
+ thread_id: "thread-triage-local-source-1",
5984
+ turn_id: "turn-triage-local-source-1",
5985
+ prompt: "search source for parseConfig in src/config.ts",
5986
+ }, { cwd });
5987
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
5988
+ assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
5989
+ assert.match(additionalContext, /Prefer the executor role surface/);
5990
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-local-source-1", "prompt-routing-state.json");
5991
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
5992
+ assert.equal(state.last_triage?.lane, "LIGHT");
5993
+ assert.equal(state.last_triage?.destination, "executor");
5994
+ }
5995
+ finally {
5996
+ await rm(cwd, { recursive: true, force: true });
5997
+ }
5998
+ });
5999
+ it("keeps anchored local API usage prompts on executor instead of researcher", async () => {
6000
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-local-api-executor-"));
6001
+ try {
6002
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
6003
+ const result = await dispatchCodexNativeHook({
6004
+ hook_event_name: "UserPromptSubmit",
6005
+ cwd,
6006
+ session_id: "triage-local-api-1",
6007
+ thread_id: "thread-triage-local-api-1",
6008
+ turn_id: "turn-triage-local-api-1",
6009
+ prompt: "find API usage in src/foo.ts",
6010
+ }, { cwd });
6011
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
6012
+ assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
6013
+ assert.match(additionalContext, /Prefer the executor role surface/);
6014
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-local-api-1", "prompt-routing-state.json");
6015
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
6016
+ assert.equal(state.last_triage?.lane, "LIGHT");
6017
+ assert.equal(state.last_triage?.destination, "executor");
6018
+ }
6019
+ finally {
6020
+ await rm(cwd, { recursive: true, force: true });
6021
+ }
6022
+ });
6023
+ it("keeps project-scoped local API usage prompts on explore instead of researcher", async () => {
6024
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-project-api-explore-"));
6025
+ try {
6026
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
6027
+ const result = await dispatchCodexNativeHook({
6028
+ hook_event_name: "UserPromptSubmit",
6029
+ cwd,
6030
+ session_id: "triage-project-api-1",
6031
+ thread_id: "thread-triage-project-api-1",
6032
+ turn_id: "turn-triage-project-api-1",
6033
+ prompt: "find API usage in this project",
6034
+ }, { cwd });
6035
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
6036
+ assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
6037
+ assert.match(additionalContext, /Prefer the explore role surface/);
6038
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-project-api-1", "prompt-routing-state.json");
6039
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
6040
+ assert.equal(state.last_triage?.lane, "LIGHT");
6041
+ assert.equal(state.last_triage?.destination, "explore");
6042
+ assert.equal(state.last_triage?.reason, "local_reference_lookup");
6043
+ }
6044
+ finally {
6045
+ await rm(cwd, { recursive: true, force: true });
6046
+ }
6047
+ });
6048
+ it("keeps repository changelog lookup prompts on explore despite generic docs terms", async () => {
6049
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-repo-changelog-explore-"));
6050
+ try {
6051
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
6052
+ const result = await dispatchCodexNativeHook({
6053
+ hook_event_name: "UserPromptSubmit",
6054
+ cwd,
6055
+ session_id: "triage-repo-changelog-1",
6056
+ thread_id: "thread-triage-repo-changelog-1",
6057
+ turn_id: "turn-triage-repo-changelog-1",
6058
+ prompt: "find changelog in this repository",
6059
+ }, { cwd });
6060
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
6061
+ assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
6062
+ assert.match(additionalContext, /Prefer the explore role surface/);
6063
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-repo-changelog-1", "prompt-routing-state.json");
6064
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
6065
+ assert.equal(state.last_triage?.lane, "LIGHT");
6066
+ assert.equal(state.last_triage?.destination, "explore");
6067
+ assert.equal(state.last_triage?.reason, "local_reference_lookup");
6068
+ }
6069
+ finally {
6070
+ await rm(cwd, { recursive: true, force: true });
6071
+ }
6072
+ });
6073
+ it("routes anchored read-only questions through explore before executor", async () => {
6074
+ const cwd = await mkdtemp(join(tmpdir(), "omx-triage-anchored-question-explore-"));
6075
+ try {
6076
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
6077
+ const result = await dispatchCodexNativeHook({
6078
+ hook_event_name: "UserPromptSubmit",
6079
+ cwd,
6080
+ session_id: "triage-anchored-question-1",
6081
+ thread_id: "thread-triage-anchored-question-1",
6082
+ turn_id: "turn-triage-anchored-question-1",
6083
+ prompt: "what does src/foo.ts do?",
6084
+ }, { cwd });
6085
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
6086
+ assert.doesNotMatch(additionalContext, /Prefer the executor role surface/);
6087
+ assert.match(additionalContext, /Prefer the explore role surface/);
6088
+ const stateFile = join(cwd, ".omx", "state", "sessions", "triage-anchored-question-1", "prompt-routing-state.json");
6089
+ const state = JSON.parse(await readFile(stateFile, "utf-8"));
6090
+ assert.equal(state.last_triage?.lane, "LIGHT");
6091
+ assert.equal(state.last_triage?.destination, "explore");
6092
+ assert.equal(state.last_triage?.reason, "question_or_explanation");
6093
+ }
6094
+ finally {
6095
+ await rm(cwd, { recursive: true, force: true });
6096
+ }
6097
+ });
4424
6098
  // ── Group 6: PASS (no triage injection, no state) ────────────────────────
4425
6099
  it("produces no triage advisory and no state for trivial greeting prompts", async () => {
4426
6100
  const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-hello-"));