oh-my-codex 0.17.3 → 0.18.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 (381) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +44 -19
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2997 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +127 -1
  11. package/crates/omx-sparkshell/src/main.rs +829 -30
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +702 -237
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
  24. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
  26. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  27. package/dist/cli/__tests__/explore.test.js +23 -0
  28. package/dist/cli/__tests__/explore.test.js.map +1 -1
  29. package/dist/cli/__tests__/index.test.js +171 -5
  30. package/dist/cli/__tests__/index.test.js.map +1 -1
  31. package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
  32. package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
  33. package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
  34. package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
  35. package/dist/cli/__tests__/launch-fallback.test.js +191 -0
  36. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  37. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  38. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  39. package/dist/cli/__tests__/question.test.js +27 -41
  40. package/dist/cli/__tests__/question.test.js.map +1 -1
  41. package/dist/cli/__tests__/setup-install-mode.test.js +232 -35
  42. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  43. package/dist/cli/__tests__/sparkshell-cli.test.js +25 -1
  44. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  45. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
  46. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  47. package/dist/cli/__tests__/ultragoal.test.js +227 -4
  48. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  49. package/dist/cli/__tests__/update.test.js +72 -1
  50. package/dist/cli/__tests__/update.test.js.map +1 -1
  51. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  52. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  53. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  54. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  55. package/dist/cli/api.d.ts +26 -0
  56. package/dist/cli/api.d.ts.map +1 -0
  57. package/dist/cli/api.js +153 -0
  58. package/dist/cli/api.js.map +1 -0
  59. package/dist/cli/codex-feature-probe.d.ts +5 -0
  60. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  61. package/dist/cli/codex-feature-probe.js +13 -7
  62. package/dist/cli/codex-feature-probe.js.map +1 -1
  63. package/dist/cli/doctor.d.ts +7 -0
  64. package/dist/cli/doctor.d.ts.map +1 -1
  65. package/dist/cli/doctor.js +119 -10
  66. package/dist/cli/doctor.js.map +1 -1
  67. package/dist/cli/explore.d.ts +2 -0
  68. package/dist/cli/explore.d.ts.map +1 -1
  69. package/dist/cli/explore.js +43 -1
  70. package/dist/cli/explore.js.map +1 -1
  71. package/dist/cli/index.d.ts +12 -4
  72. package/dist/cli/index.d.ts.map +1 -1
  73. package/dist/cli/index.js +460 -87
  74. package/dist/cli/index.js.map +1 -1
  75. package/dist/cli/native-assets.d.ts +2 -1
  76. package/dist/cli/native-assets.d.ts.map +1 -1
  77. package/dist/cli/native-assets.js +1 -0
  78. package/dist/cli/native-assets.js.map +1 -1
  79. package/dist/cli/plugin-marketplace.d.ts +2 -0
  80. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  81. package/dist/cli/plugin-marketplace.js +15 -1
  82. package/dist/cli/plugin-marketplace.js.map +1 -1
  83. package/dist/cli/setup.d.ts.map +1 -1
  84. package/dist/cli/setup.js +71 -11
  85. package/dist/cli/setup.js.map +1 -1
  86. package/dist/cli/sparkshell.d.ts +7 -1
  87. package/dist/cli/sparkshell.d.ts.map +1 -1
  88. package/dist/cli/sparkshell.js +31 -4
  89. package/dist/cli/sparkshell.js.map +1 -1
  90. package/dist/cli/ultragoal.d.ts +1 -1
  91. package/dist/cli/ultragoal.d.ts.map +1 -1
  92. package/dist/cli/ultragoal.js +184 -10
  93. package/dist/cli/ultragoal.js.map +1 -1
  94. package/dist/cli/update.d.ts +2 -0
  95. package/dist/cli/update.d.ts.map +1 -1
  96. package/dist/cli/update.js +14 -3
  97. package/dist/cli/update.js.map +1 -1
  98. package/dist/compat/__tests__/doctor-contract.test.js +3 -0
  99. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  100. package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
  101. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
  102. package/dist/config/__tests__/codex-hooks.test.js +19 -8
  103. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  104. package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
  105. package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
  106. package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
  107. package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
  108. package/dist/config/codex-feature-flags.d.ts +4 -0
  109. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  110. package/dist/config/codex-feature-flags.js +4 -0
  111. package/dist/config/codex-feature-flags.js.map +1 -1
  112. package/dist/config/codex-hooks.js +6 -6
  113. package/dist/config/codex-hooks.js.map +1 -1
  114. package/dist/config/commit-lore-guard.d.ts +1 -0
  115. package/dist/config/commit-lore-guard.d.ts.map +1 -1
  116. package/dist/config/commit-lore-guard.js +29 -3
  117. package/dist/config/commit-lore-guard.js.map +1 -1
  118. package/dist/config/generator.d.ts +3 -1
  119. package/dist/config/generator.d.ts.map +1 -1
  120. package/dist/config/generator.js +114 -10
  121. package/dist/config/generator.js.map +1 -1
  122. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
  123. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  124. package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
  125. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  126. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
  127. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  128. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  129. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  130. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  131. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  132. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
  133. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
  134. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  135. package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
  136. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  137. package/dist/hooks/__tests__/keyword-detector.test.js +15 -3
  138. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  139. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  140. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  141. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
  142. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  143. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  144. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  145. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
  146. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  147. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  148. package/dist/hooks/extensibility/dispatcher.js +29 -14
  149. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  150. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  151. package/dist/hooks/keyword-detector.js +8 -3
  152. package/dist/hooks/keyword-detector.js.map +1 -1
  153. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  154. package/dist/hooks/keyword-registry.js +1 -0
  155. package/dist/hooks/keyword-registry.js.map +1 -1
  156. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  157. package/dist/hooks/prompt-guidance-contract.js +3 -2
  158. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  159. package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
  160. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  161. package/dist/hud/__tests__/reconcile.test.js +4 -4
  162. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  163. package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
  164. package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  165. package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
  166. package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
  167. package/dist/hud/__tests__/tmux.test.js +23 -18
  168. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  169. package/dist/hud/index.d.ts +1 -1
  170. package/dist/hud/index.d.ts.map +1 -1
  171. package/dist/hud/index.js +10 -4
  172. package/dist/hud/index.js.map +1 -1
  173. package/dist/hud/tmux.d.ts.map +1 -1
  174. package/dist/hud/tmux.js +9 -8
  175. package/dist/hud/tmux.js.map +1 -1
  176. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  177. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  178. package/dist/mcp/bootstrap.d.ts +3 -1
  179. package/dist/mcp/bootstrap.d.ts.map +1 -1
  180. package/dist/mcp/bootstrap.js +71 -2
  181. package/dist/mcp/bootstrap.js.map +1 -1
  182. package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
  183. package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
  184. package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
  185. package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
  186. package/dist/notifications/__tests__/verbosity.test.js +20 -0
  187. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  188. package/dist/notifications/config.d.ts.map +1 -1
  189. package/dist/notifications/config.js +6 -3
  190. package/dist/notifications/config.js.map +1 -1
  191. package/dist/notifications/http-client.d.ts.map +1 -1
  192. package/dist/notifications/http-client.js +78 -27
  193. package/dist/notifications/http-client.js.map +1 -1
  194. package/dist/notifications/types.d.ts +2 -0
  195. package/dist/notifications/types.d.ts.map +1 -1
  196. package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
  197. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  198. package/dist/openclaw/dispatcher.d.ts +7 -4
  199. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  200. package/dist/openclaw/dispatcher.js +32 -69
  201. package/dist/openclaw/dispatcher.js.map +1 -1
  202. package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
  203. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  204. package/dist/pipeline/__tests__/stages.test.js +50 -5
  205. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  206. package/dist/pipeline/index.d.ts +8 -2
  207. package/dist/pipeline/index.d.ts.map +1 -1
  208. package/dist/pipeline/index.js +5 -2
  209. package/dist/pipeline/index.js.map +1 -1
  210. package/dist/pipeline/orchestrator.d.ts +5 -4
  211. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  212. package/dist/pipeline/orchestrator.js +56 -15
  213. package/dist/pipeline/orchestrator.js.map +1 -1
  214. package/dist/pipeline/stages/code-review.d.ts +2 -2
  215. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  216. package/dist/pipeline/stages/code-review.js +5 -3
  217. package/dist/pipeline/stages/code-review.js.map +1 -1
  218. package/dist/pipeline/stages/deep-interview.d.ts +15 -0
  219. package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
  220. package/dist/pipeline/stages/deep-interview.js +32 -0
  221. package/dist/pipeline/stages/deep-interview.js.map +1 -0
  222. package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
  223. package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
  224. package/dist/pipeline/stages/ralph-verify.js +2 -2
  225. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  226. package/dist/pipeline/stages/ultragoal.d.ts +19 -0
  227. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
  228. package/dist/pipeline/stages/ultragoal.js +38 -0
  229. package/dist/pipeline/stages/ultragoal.js.map +1 -0
  230. package/dist/pipeline/stages/ultraqa.d.ts +30 -0
  231. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
  232. package/dist/pipeline/stages/ultraqa.js +46 -0
  233. package/dist/pipeline/stages/ultraqa.js.map +1 -0
  234. package/dist/pipeline/types.d.ts +8 -6
  235. package/dist/pipeline/types.d.ts.map +1 -1
  236. package/dist/pipeline/types.js +2 -2
  237. package/dist/scripts/__tests__/codex-native-hook.test.js +1488 -117
  238. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  239. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  240. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  241. package/dist/scripts/__tests__/smoke-packed-install.test.js +27 -2
  242. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  243. package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
  244. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  245. package/dist/scripts/build-api.d.ts +2 -0
  246. package/dist/scripts/build-api.d.ts.map +1 -0
  247. package/dist/scripts/build-api.js +44 -0
  248. package/dist/scripts/build-api.js.map +1 -0
  249. package/dist/scripts/cleanup-explore-harness.js +1 -0
  250. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  251. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  252. package/dist/scripts/codex-native-hook.js +364 -16
  253. package/dist/scripts/codex-native-hook.js.map +1 -1
  254. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  255. package/dist/scripts/codex-native-pre-post.js +98 -25
  256. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  257. package/dist/scripts/notify-dispatcher.js +88 -0
  258. package/dist/scripts/notify-dispatcher.js.map +1 -1
  259. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  260. package/dist/scripts/notify-hook/process-runner.js +39 -17
  261. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  262. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  263. package/dist/scripts/notify-hook/team-dispatch.js +36 -14
  264. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  265. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  266. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  267. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  268. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +2 -1
  269. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  270. package/dist/scripts/notify-hook/team-tmux-guard.js +45 -1
  271. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  272. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  273. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  274. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  275. package/dist/scripts/run-provider-advisor.js +9 -3
  276. package/dist/scripts/run-provider-advisor.js.map +1 -1
  277. package/dist/scripts/smoke-packed-install.d.ts +4 -1
  278. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  279. package/dist/scripts/smoke-packed-install.js +101 -1
  280. package/dist/scripts/smoke-packed-install.js.map +1 -1
  281. package/dist/scripts/sync-plugin-mirror.js +2 -2
  282. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  283. package/dist/scripts/verify-native-agents.js +2 -2
  284. package/dist/scripts/verify-native-agents.js.map +1 -1
  285. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
  286. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  287. package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
  288. package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
  289. package/dist/sidecar/index.d.ts +1 -1
  290. package/dist/sidecar/index.d.ts.map +1 -1
  291. package/dist/sidecar/index.js +29 -12
  292. package/dist/sidecar/index.js.map +1 -1
  293. package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
  294. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  295. package/dist/state/operations.d.ts.map +1 -1
  296. package/dist/state/operations.js +11 -0
  297. package/dist/state/operations.js.map +1 -1
  298. package/dist/team/__tests__/runtime.test.js +2 -2
  299. package/dist/team/__tests__/runtime.test.js.map +1 -1
  300. package/dist/team/__tests__/tmux-session.test.js +207 -22
  301. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  302. package/dist/team/tmux-session.d.ts +1 -0
  303. package/dist/team/tmux-session.d.ts.map +1 -1
  304. package/dist/team/tmux-session.js +73 -28
  305. package/dist/team/tmux-session.js.map +1 -1
  306. package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
  307. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  308. package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
  309. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  310. package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
  311. package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
  312. package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
  313. package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
  314. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
  315. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
  316. package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
  317. package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
  318. package/dist/ultragoal/artifacts.d.ts +97 -2
  319. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  320. package/dist/ultragoal/artifacts.js +811 -256
  321. package/dist/ultragoal/artifacts.js.map +1 -1
  322. package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
  323. package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
  324. package/dist/utils/__tests__/sleep-resource.test.js +39 -0
  325. package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
  326. package/dist/utils/sleep.d.ts.map +1 -1
  327. package/dist/utils/sleep.js +17 -6
  328. package/dist/utils/sleep.js.map +1 -1
  329. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  330. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  331. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  332. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  333. package/package.json +5 -3
  334. package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
  335. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
  336. package/plugins/oh-my-codex/hooks/hooks.json +77 -0
  337. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
  338. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  339. package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
  340. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -8
  341. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  342. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
  343. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
  344. package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
  345. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -5
  346. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  347. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
  348. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
  349. package/prompts/planner.md +1 -1
  350. package/prompts/researcher.md +15 -10
  351. package/skills/autopilot/SKILL.md +77 -47
  352. package/skills/best-practice-research/SKILL.md +83 -0
  353. package/skills/cancel/SKILL.md +2 -2
  354. package/skills/deep-interview/SKILL.md +9 -8
  355. package/skills/omx-setup/SKILL.md +1 -1
  356. package/skills/pipeline/SKILL.md +22 -11
  357. package/skills/plan/SKILL.md +8 -8
  358. package/skills/ralph/SKILL.md +7 -0
  359. package/skills/ralplan/SKILL.md +5 -5
  360. package/skills/team/SKILL.md +1 -1
  361. package/skills/ultragoal/SKILL.md +38 -4
  362. package/skills/ultrawork/SKILL.md +1 -1
  363. package/src/scripts/__tests__/codex-native-hook.test.ts +1758 -166
  364. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  365. package/src/scripts/__tests__/smoke-packed-install.test.ts +39 -2
  366. package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
  367. package/src/scripts/build-api.ts +48 -0
  368. package/src/scripts/cleanup-explore-harness.ts +1 -0
  369. package/src/scripts/codex-native-hook.ts +416 -18
  370. package/src/scripts/codex-native-pre-post.ts +119 -25
  371. package/src/scripts/notify-dispatcher.ts +97 -0
  372. package/src/scripts/notify-hook/process-runner.ts +40 -16
  373. package/src/scripts/notify-hook/team-dispatch.ts +36 -13
  374. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  375. package/src/scripts/notify-hook/team-tmux-guard.ts +49 -0
  376. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  377. package/src/scripts/run-provider-advisor.ts +11 -3
  378. package/src/scripts/smoke-packed-install.ts +107 -0
  379. package/src/scripts/sync-plugin-mirror.ts +3 -3
  380. package/src/scripts/verify-native-agents.ts +2 -2
  381. package/templates/catalog-manifest.json +7 -0
@@ -17,6 +17,7 @@ import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
17
17
  import { readAllState } from "../../hud/state.js";
18
18
  import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
19
19
  import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
20
+ import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
20
21
  function nativeHookScriptPath() {
21
22
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
22
23
  }
@@ -39,6 +40,38 @@ async function writeJson(path, value) {
39
40
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
40
41
  await writeFile(path, JSON.stringify(value, null, 2));
41
42
  }
43
+ async function withLoreGuardConfig(value, prefix, run) {
44
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-pretool-git-commit-lore-${prefix}-`));
45
+ const codexHome = await mkdtemp(join(tmpdir(), `omx-native-hook-codex-home-lore-${prefix}-`));
46
+ const defaultHome = await mkdtemp(join(tmpdir(), `omx-native-hook-home-lore-${prefix}-`));
47
+ const originalGuard = process.env.OMX_LORE_COMMIT_GUARD;
48
+ const originalCodexHome = process.env.CODEX_HOME;
49
+ const originalHome = process.env.HOME;
50
+ try {
51
+ delete process.env.OMX_LORE_COMMIT_GUARD;
52
+ process.env.CODEX_HOME = codexHome;
53
+ process.env.HOME = defaultHome;
54
+ await writeFile(join(codexHome, "config.toml"), `[shell_environment_policy.set]\nOMX_LORE_COMMIT_GUARD = "${value}"\n`, "utf-8");
55
+ return await run(cwd);
56
+ }
57
+ finally {
58
+ if (originalGuard === undefined)
59
+ delete process.env.OMX_LORE_COMMIT_GUARD;
60
+ else
61
+ process.env.OMX_LORE_COMMIT_GUARD = originalGuard;
62
+ if (originalCodexHome === undefined)
63
+ delete process.env.CODEX_HOME;
64
+ else
65
+ process.env.CODEX_HOME = originalCodexHome;
66
+ if (originalHome === undefined)
67
+ delete process.env.HOME;
68
+ else
69
+ process.env.HOME = originalHome;
70
+ await rm(cwd, { recursive: true, force: true });
71
+ await rm(codexHome, { recursive: true, force: true });
72
+ await rm(defaultHome, { recursive: true, force: true });
73
+ }
74
+ }
42
75
  function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
43
76
  return `#!/usr/bin/env bash
44
77
  set -eu
@@ -67,7 +100,7 @@ if [[ "$cmd" == "display-message" ]]; then
67
100
  exit 0
68
101
  fi
69
102
  if [[ "$cmd" == "capture-pane" ]]; then
70
- echo "› ready"
103
+ ${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
71
104
  exit 0
72
105
  fi
73
106
  if [[ "$cmd" == "send-keys" ]]; then
@@ -567,7 +600,16 @@ describe("codex native hook dispatch", () => {
567
600
  });
568
601
  it("keeps subagent SessionStart from replacing the canonical leader session", async () => {
569
602
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-"));
570
- try {
603
+ const originalCodexHome = process.env.CODEX_HOME;
604
+ try {
605
+ process.env.CODEX_HOME = join(cwd, "codex-home");
606
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
607
+ notifications: {
608
+ enabled: true,
609
+ verbosity: "session",
610
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
611
+ },
612
+ });
571
613
  const stateDir = join(cwd, ".omx", "state");
572
614
  const canonicalSessionId = "omx-leader-session";
573
615
  const leaderNativeSessionId = "codex-leader-thread";
@@ -583,6 +625,13 @@ describe("codex native hook dispatch", () => {
583
625
  iteration: 1,
584
626
  max_iterations: 5,
585
627
  });
628
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
629
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
630
+ "import { appendFileSync } from 'node:fs';",
631
+ "export async function onHookEvent(event) {",
632
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event, context: event.context })}\\n`);",
633
+ "}",
634
+ ].join("\n"));
586
635
  const transcriptPath = join(cwd, "subagent-rollout.jsonl");
587
636
  await writeFile(transcriptPath, `${JSON.stringify({
588
637
  type: "session_meta",
@@ -616,6 +665,7 @@ describe("codex native hook dispatch", () => {
616
665
  const leaderRalph = JSON.parse(await readFile(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), "utf-8"));
617
666
  assert.equal(leaderRalph.active, true);
618
667
  assert.equal(leaderRalph.current_phase, "executing");
668
+ assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent SessionStart must not independently dispatch session-start hook notifications");
619
669
  const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
620
670
  assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, leaderNativeSessionId);
621
671
  assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
@@ -623,8 +673,213 @@ describe("codex native hook dispatch", () => {
623
673
  assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
624
674
  assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
625
675
  assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
676
+ await dispatchCodexNativeHook({
677
+ hook_event_name: "Stop",
678
+ cwd,
679
+ session_id: childNativeSessionId,
680
+ thread_id: childNativeSessionId,
681
+ turn_id: "child-stop-turn",
682
+ }, { cwd });
683
+ assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent Stop must not independently dispatch stop hook notifications");
684
+ }
685
+ finally {
686
+ if (originalCodexHome === undefined) {
687
+ delete process.env.CODEX_HOME;
688
+ }
689
+ else {
690
+ process.env.CODEX_HOME = originalCodexHome;
691
+ }
692
+ await rm(cwd, { recursive: true, force: true });
693
+ }
694
+ });
695
+ it("suppresses child-agent SessionStart hook dispatch at minimal verbosity", async () => {
696
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-minimal-"));
697
+ const originalCodexHome = process.env.CODEX_HOME;
698
+ try {
699
+ process.env.CODEX_HOME = join(cwd, "codex-home");
700
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
701
+ notifications: {
702
+ enabled: true,
703
+ verbosity: "minimal",
704
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
705
+ },
706
+ });
707
+ const stateDir = join(cwd, ".omx", "state");
708
+ const canonicalSessionId = "omx-leader-session-minimal";
709
+ const leaderNativeSessionId = "codex-leader-thread-minimal";
710
+ const childNativeSessionId = "codex-child-thread-minimal";
711
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
712
+ await writeSessionStart(cwd, canonicalSessionId, {
713
+ nativeSessionId: leaderNativeSessionId,
714
+ });
715
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
716
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
717
+ "import { appendFileSync } from 'node:fs';",
718
+ "export async function onHookEvent(event) {",
719
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
720
+ "}",
721
+ ].join("\n"));
722
+ const transcriptPath = join(cwd, "minimal-subagent-rollout.jsonl");
723
+ await writeFile(transcriptPath, `${JSON.stringify({
724
+ type: "session_meta",
725
+ payload: {
726
+ id: childNativeSessionId,
727
+ source: {
728
+ subagent: {
729
+ thread_spawn: {
730
+ parent_thread_id: leaderNativeSessionId,
731
+ agent_role: "verifier",
732
+ },
733
+ },
734
+ },
735
+ },
736
+ })}\n`);
737
+ await dispatchCodexNativeHook({
738
+ hook_event_name: "SessionStart",
739
+ cwd,
740
+ session_id: childNativeSessionId,
741
+ transcript_path: transcriptPath,
742
+ }, { cwd, sessionOwnerPid: process.pid });
743
+ assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent SessionStart must be suppressed at minimal verbosity");
744
+ }
745
+ finally {
746
+ if (originalCodexHome === undefined) {
747
+ delete process.env.CODEX_HOME;
748
+ }
749
+ else {
750
+ process.env.CODEX_HOME = originalCodexHome;
751
+ }
752
+ await rm(cwd, { recursive: true, force: true });
753
+ }
754
+ });
755
+ it("allows explicit child-agent lifecycle hook dispatch when includeChildAgents is enabled", async () => {
756
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-include-"));
757
+ const originalCodexHome = process.env.CODEX_HOME;
758
+ try {
759
+ process.env.CODEX_HOME = join(cwd, "codex-home");
760
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
761
+ notifications: {
762
+ enabled: true,
763
+ verbosity: "session",
764
+ includeChildAgents: true,
765
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
766
+ },
767
+ });
768
+ const stateDir = join(cwd, ".omx", "state");
769
+ const canonicalSessionId = "omx-leader-session-include";
770
+ const leaderNativeSessionId = "codex-leader-thread-include";
771
+ const childNativeSessionId = "codex-child-thread-include";
772
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
773
+ await writeSessionStart(cwd, canonicalSessionId, {
774
+ nativeSessionId: leaderNativeSessionId,
775
+ });
776
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
777
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
778
+ "import { appendFileSync } from 'node:fs';",
779
+ "export async function onHookEvent(event) {",
780
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
781
+ "}",
782
+ ].join("\n"));
783
+ const transcriptPath = join(cwd, "included-subagent-rollout.jsonl");
784
+ await writeFile(transcriptPath, `${JSON.stringify({
785
+ type: "session_meta",
786
+ payload: {
787
+ id: childNativeSessionId,
788
+ source: {
789
+ subagent: {
790
+ thread_spawn: {
791
+ parent_thread_id: leaderNativeSessionId,
792
+ agent_role: "verifier",
793
+ },
794
+ },
795
+ },
796
+ },
797
+ })}\n`);
798
+ await dispatchCodexNativeHook({
799
+ hook_event_name: "SessionStart",
800
+ cwd,
801
+ session_id: childNativeSessionId,
802
+ transcript_path: transcriptPath,
803
+ }, { cwd, sessionOwnerPid: process.pid });
804
+ await dispatchCodexNativeHook({
805
+ hook_event_name: "Stop",
806
+ cwd,
807
+ session_id: childNativeSessionId,
808
+ thread_id: childNativeSessionId,
809
+ turn_id: "included-child-stop-turn",
810
+ }, { cwd });
811
+ const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
812
+ assert.match(hookEvents, /"event":"session-start"/);
813
+ assert.match(hookEvents, /"event":"stop"/);
814
+ }
815
+ finally {
816
+ if (originalCodexHome === undefined) {
817
+ delete process.env.CODEX_HOME;
818
+ }
819
+ else {
820
+ process.env.CODEX_HOME = originalCodexHome;
821
+ }
822
+ await rm(cwd, { recursive: true, force: true });
823
+ }
824
+ });
825
+ it("allows child-agent lifecycle hook dispatch at agent verbosity", async () => {
826
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-agent-"));
827
+ const originalCodexHome = process.env.CODEX_HOME;
828
+ try {
829
+ process.env.CODEX_HOME = join(cwd, "codex-home");
830
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
831
+ notifications: {
832
+ enabled: true,
833
+ verbosity: "agent",
834
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
835
+ },
836
+ });
837
+ const stateDir = join(cwd, ".omx", "state");
838
+ const canonicalSessionId = "omx-leader-session-agent";
839
+ const leaderNativeSessionId = "codex-leader-thread-agent";
840
+ const childNativeSessionId = "codex-child-thread-agent";
841
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
842
+ await writeSessionStart(cwd, canonicalSessionId, {
843
+ nativeSessionId: leaderNativeSessionId,
844
+ });
845
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
846
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
847
+ "import { appendFileSync } from 'node:fs';",
848
+ "export async function onHookEvent(event) {",
849
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
850
+ "}",
851
+ ].join("\n"));
852
+ const transcriptPath = join(cwd, "agent-verbosity-subagent-rollout.jsonl");
853
+ await writeFile(transcriptPath, `${JSON.stringify({
854
+ type: "session_meta",
855
+ payload: {
856
+ id: childNativeSessionId,
857
+ source: {
858
+ subagent: {
859
+ thread_spawn: {
860
+ parent_thread_id: leaderNativeSessionId,
861
+ agent_role: "verifier",
862
+ },
863
+ },
864
+ },
865
+ },
866
+ })}\n`);
867
+ await dispatchCodexNativeHook({
868
+ hook_event_name: "SessionStart",
869
+ cwd,
870
+ session_id: childNativeSessionId,
871
+ transcript_path: transcriptPath,
872
+ }, { cwd, sessionOwnerPid: process.pid });
873
+ const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
874
+ assert.match(hookEvents, /"event":"session-start"/);
626
875
  }
627
876
  finally {
877
+ if (originalCodexHome === undefined) {
878
+ delete process.env.CODEX_HOME;
879
+ }
880
+ else {
881
+ process.env.CODEX_HOME = originalCodexHome;
882
+ }
628
883
  await rm(cwd, { recursive: true, force: true });
629
884
  }
630
885
  });
@@ -1218,6 +1473,62 @@ describe("codex native hook dispatch", () => {
1218
1473
  await rm(cwd, { recursive: true, force: true });
1219
1474
  }
1220
1475
  });
1476
+ it("does not repeat performance-goal reconciliation after a recorded objective mismatch blocker", async () => {
1477
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-mismatch-blocked-stop-"));
1478
+ try {
1479
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
1480
+ version: 1,
1481
+ workflow: "performance-goal",
1482
+ slug: "latency",
1483
+ objective: "Reduce latency",
1484
+ status: "blocked",
1485
+ lastValidation: {
1486
+ status: "blocked",
1487
+ evidence: "omx performance-goal complete rejected the fresh get_goal snapshot: Codex goal objective mismatch: expected \"reduce latency\", got \"legacy objective\".",
1488
+ recordedAt: "2026-05-20T00:00:00.000Z",
1489
+ },
1490
+ });
1491
+ const result = await dispatchCodexNativeHook({
1492
+ hook_event_name: "Stop",
1493
+ cwd,
1494
+ session_id: "sess-performance-mismatch-blocked-stop",
1495
+ thread_id: "thread-performance-mismatch-blocked-stop",
1496
+ last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
1497
+ }, { cwd });
1498
+ assert.notEqual(result.outputJson?.decision, "block");
1499
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
1500
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1501
+ }
1502
+ finally {
1503
+ await rm(cwd, { recursive: true, force: true });
1504
+ }
1505
+ });
1506
+ it("does not block Stop for an already complete performance-goal state", async () => {
1507
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-complete-stop-"));
1508
+ try {
1509
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
1510
+ version: 1,
1511
+ workflow: "performance-goal",
1512
+ slug: "latency",
1513
+ objective: "Reduce latency",
1514
+ status: "complete",
1515
+ completedAt: "2026-05-20T00:00:00.000Z",
1516
+ });
1517
+ const result = await dispatchCodexNativeHook({
1518
+ hook_event_name: "Stop",
1519
+ cwd,
1520
+ session_id: "sess-performance-complete-stop",
1521
+ thread_id: "thread-performance-complete-stop",
1522
+ last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
1523
+ }, { cwd });
1524
+ assert.notEqual(result.outputJson?.decision, "block");
1525
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
1526
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1527
+ }
1528
+ finally {
1529
+ await rm(cwd, { recursive: true, force: true });
1530
+ }
1531
+ });
1221
1532
  it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
1222
1533
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
1223
1534
  try {
@@ -1262,7 +1573,7 @@ describe("codex native hook dispatch", () => {
1262
1573
  await rm(cwd, { recursive: true, force: true });
1263
1574
  }
1264
1575
  });
1265
- it("blocks ultragoal Stop with blocked checkpoint and fresh-thread remediation for completed legacy snapshots", async () => {
1576
+ it("blocks ultragoal Stop with blocked checkpoint and available-goal-context remediation for completed legacy snapshots", async () => {
1266
1577
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
1267
1578
  try {
1268
1579
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
@@ -1281,7 +1592,8 @@ describe("codex native hook dispatch", () => {
1281
1592
  assert.equal(result.outputJson?.decision, "block");
1282
1593
  assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
1283
1594
  assert.match(output, /--status blocked/);
1284
- assert.match(output, /fresh Codex thread/);
1595
+ assert.match(output, /Codex goal context/);
1596
+ assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
1285
1597
  assert.match(output, /Hooks must not mutate Codex goal state/);
1286
1598
  }
1287
1599
  finally {
@@ -1294,7 +1606,7 @@ describe("codex native hook dispatch", () => {
1294
1606
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
1295
1607
  version: 1,
1296
1608
  codexGoalMode: "aggregate",
1297
- codexObjective: "Complete all ultragoal stories in .omx/ultragoal/goals.json: many micro goals",
1609
+ codexObjective: "Complete the durable ultragoal plan in .omx/ultragoal/goals.json, including later accepted/appended stories, under the original brief constraints; use .omx/ultragoal/ledger.jsonl as the audit trail.",
1298
1610
  activeGoalId: "G001-micro",
1299
1611
  aggregateCompletion: {
1300
1612
  status: "complete",
@@ -1409,6 +1721,67 @@ describe("codex native hook dispatch", () => {
1409
1721
  await rm(cwd, { recursive: true, force: true });
1410
1722
  }
1411
1723
  });
1724
+ it("does not repeat Stop block when the last autoresearch-goal completion attempt reported objective mismatch", async () => {
1725
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-reported-stop-"));
1726
+ try {
1727
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "mission.json"), {
1728
+ version: 1,
1729
+ workflow: "autoresearch-goal",
1730
+ slug: "mismatched-mission",
1731
+ topic: "Passing research bound to another Codex goal",
1732
+ status: "passed",
1733
+ });
1734
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "completion.json"), {
1735
+ verdict: "pass",
1736
+ passed: true,
1737
+ });
1738
+ const result = await dispatchCodexNativeHook({
1739
+ hook_event_name: "Stop",
1740
+ cwd,
1741
+ session_id: "sess-autoresearch-mismatch-reported-stop",
1742
+ thread_id: "thread-autoresearch-mismatch-reported-stop",
1743
+ last_assistant_message: [
1744
+ "I called get_goal and ran omx autoresearch-goal complete --slug mismatched-mission --codex-goal-json /tmp/snapshot.json.",
1745
+ "The autoresearch-goal completion failed with Codex goal objective mismatch, so I will not repeat the same complete command blindly in this thread.",
1746
+ ].join("\n"),
1747
+ }, { cwd });
1748
+ assert.notEqual(result.outputJson?.decision, "block");
1749
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /autoresearch-goal complete --slug mismatched-mission/);
1750
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1751
+ }
1752
+ finally {
1753
+ await rm(cwd, { recursive: true, force: true });
1754
+ }
1755
+ });
1756
+ it("still blocks later autoresearch-goal completion claims after an objective mismatch if no mismatch is reported in the final answer", async () => {
1757
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-later-retry-stop-"));
1758
+ try {
1759
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "mission.json"), {
1760
+ version: 1,
1761
+ workflow: "autoresearch-goal",
1762
+ slug: "retryable-mission",
1763
+ topic: "Passing research that can still retry with the correct snapshot",
1764
+ status: "passed",
1765
+ });
1766
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "completion.json"), {
1767
+ verdict: "pass",
1768
+ passed: true,
1769
+ });
1770
+ const result = await dispatchCodexNativeHook({
1771
+ hook_event_name: "Stop",
1772
+ cwd,
1773
+ session_id: "sess-autoresearch-mismatch-later-retry-stop",
1774
+ thread_id: "thread-autoresearch-mismatch-later-retry-stop",
1775
+ last_assistant_message: "Autoresearch goal complete; next call update_goal({status: \"complete\"}).",
1776
+ }, { cwd });
1777
+ assert.equal(result.outputJson?.decision, "block");
1778
+ assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1779
+ assert.match(JSON.stringify(result.outputJson), /omx autoresearch-goal complete --slug retryable-mission/);
1780
+ }
1781
+ finally {
1782
+ await rm(cwd, { recursive: true, force: true });
1783
+ }
1784
+ });
1412
1785
  it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
1413
1786
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
1414
1787
  try {
@@ -1512,49 +1885,205 @@ describe("codex native hook dispatch", () => {
1512
1885
  assert.match(message, /get_goal/);
1513
1886
  assert.match(message, /create_goal/);
1514
1887
  assert.match(message, /update_goal/);
1888
+ assert.match(message, /does not call `\/goal clear`/);
1889
+ assert.match(message, /multiple sequential ultragoal runs/);
1515
1890
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
1516
1891
  }
1517
1892
  finally {
1518
1893
  await rm(cwd, { recursive: true, force: true });
1519
1894
  }
1520
1895
  });
1521
- it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
1522
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
1896
+ it("applies only explicit structured UserPromptSubmit ultragoal steering directives", async () => {
1897
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-"));
1523
1898
  try {
1524
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1899
+ await createUltragoalPlan(cwd, {
1900
+ brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal hook steering fixture",
1901
+ goals: [{ title: "First", objective: "Complete first milestone with tests." }],
1902
+ });
1903
+ const prose = await dispatchCodexNativeHook({
1904
+ hook_event_name: "UserPromptSubmit",
1905
+ cwd,
1906
+ session_id: "sess-ultragoal-steer-1",
1907
+ prompt: "Please add a subgoal for docs later; this is normal prose, not a directive.",
1908
+ }, { cwd });
1909
+ assert.equal(prose.outputJson, null);
1910
+ assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
1911
+ const jsonExample = await dispatchCodexNativeHook({
1912
+ hook_event_name: "UserPromptSubmit",
1913
+ cwd,
1914
+ session_id: "sess-ultragoal-steer-1",
1915
+ prompt: `Here is an inert example:\n\`\`\`json\n${JSON.stringify({
1916
+ kind: "add_subgoal",
1917
+ source: "user_prompt_submit",
1918
+ evidence: "Example JSON should not mutate .omx/ultragoal.",
1919
+ rationale: "Only explicit steering fences or labels are executable.",
1920
+ title: "Inert JSON example",
1921
+ objective: "This example must not be added.",
1922
+ })}\n\`\`\``,
1923
+ }, { cwd });
1924
+ assert.equal(jsonExample.outputJson, null);
1925
+ assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
1525
1926
  const result = await dispatchCodexNativeHook({
1526
1927
  hook_event_name: "UserPromptSubmit",
1527
1928
  cwd,
1528
- session_id: "sess-ulw-ko",
1529
- thread_id: "thread-ulw-ko",
1530
- turn_id: "turn-ulw-ko",
1531
- prompt: "ㅕㅣㅈ로 병렬 처리해줘",
1929
+ session_id: "sess-ultragoal-steer-1",
1930
+ prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
1931
+ kind: "add_subgoal",
1932
+ source: "user_prompt_submit",
1933
+ evidence: "Prompt-submit supplied a structured .omx/ultragoal directive for G002-cli-and-prompt-submit-bridge.",
1934
+ rationale: "Add bounded hook regression work while preserving all completion gates.",
1935
+ title: "Prompt bridge regression",
1936
+ objective: "Verify UserPromptSubmit bounded steering bridge with tests.",
1937
+ })}`,
1532
1938
  }, { cwd });
1533
- assert.equal(result.omxEventName, "keyword-detector");
1534
- assert.equal(result.skillState?.skill, "ultrawork");
1535
- assert.equal(result.skillState?.keyword, "ulw");
1536
- const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
1537
- assert.match(additionalContext, /workflow keyword \"ulw\" -> ultrawork/);
1538
- assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ulw-ko", "ultrawork-state.json")), true);
1939
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
1940
+ assert.match(message, /bounded \.omx\/ultragoal steering/);
1941
+ assert.match(message, /G002-cli-and-prompt-submit-bridge/);
1942
+ assert.match(message, /accepted/);
1943
+ const plan = await readUltragoalPlan(cwd);
1944
+ assert.equal(plan.goals.length, 2);
1945
+ assert.equal(plan.goals[1]?.title, "Prompt bridge regression");
1539
1946
  }
1540
1947
  finally {
1541
1948
  await rm(cwd, { recursive: true, force: true });
1542
1949
  }
1543
1950
  });
1544
- it("adds ultrawork-specific activation guidance only for true ultrawork workflow activation", async () => {
1545
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultrawork-routing-"));
1951
+ it("does not apply UserPromptSubmit ultragoal steering from native subagent prompts", async () => {
1952
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-subagent-"));
1546
1953
  try {
1547
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1548
- const result = await dispatchCodexNativeHook({
1549
- hook_event_name: "UserPromptSubmit",
1550
- cwd,
1551
- session_id: "sess-ultrawork-msg",
1552
- thread_id: "thread-ultrawork-msg",
1553
- turn_id: "turn-ultrawork-msg",
1554
- prompt: "$ultrawork fan out the regression checks",
1555
- }, { cwd });
1556
- assert.equal(result.omxEventName, "keyword-detector");
1557
- assert.equal(result.skillState?.skill, "ultrawork");
1954
+ await createUltragoalPlan(cwd, {
1955
+ brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal subagent steering fixture",
1956
+ goals: [{ title: "First", objective: "Complete first milestone with tests." }],
1957
+ });
1958
+ const stateDir = join(cwd, ".omx", "state");
1959
+ const canonicalSessionId = "sess-ultragoal-parent";
1960
+ const leaderNativeSessionId = "native-ultragoal-parent";
1961
+ const childNativeSessionId = "native-ultragoal-child";
1962
+ const nowIso = new Date().toISOString();
1963
+ await writeJson(join(stateDir, "session.json"), {
1964
+ session_id: canonicalSessionId,
1965
+ native_session_id: leaderNativeSessionId,
1966
+ });
1967
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
1968
+ schemaVersion: 1,
1969
+ sessions: {
1970
+ [canonicalSessionId]: {
1971
+ session_id: canonicalSessionId,
1972
+ leader_thread_id: leaderNativeSessionId,
1973
+ updated_at: nowIso,
1974
+ threads: {
1975
+ [leaderNativeSessionId]: {
1976
+ thread_id: leaderNativeSessionId,
1977
+ kind: "leader",
1978
+ first_seen_at: nowIso,
1979
+ last_seen_at: nowIso,
1980
+ turn_count: 1,
1981
+ },
1982
+ [childNativeSessionId]: {
1983
+ thread_id: childNativeSessionId,
1984
+ kind: "subagent",
1985
+ first_seen_at: nowIso,
1986
+ last_seen_at: nowIso,
1987
+ turn_count: 1,
1988
+ mode: "architect",
1989
+ },
1990
+ },
1991
+ },
1992
+ },
1993
+ });
1994
+ const result = await dispatchCodexNativeHook({
1995
+ hook_event_name: "UserPromptSubmit",
1996
+ cwd,
1997
+ session_id: childNativeSessionId,
1998
+ thread_id: childNativeSessionId,
1999
+ turn_id: "turn-ultragoal-child-1",
2000
+ prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
2001
+ kind: "add_subgoal",
2002
+ source: "user_prompt_submit",
2003
+ evidence: "Subagent prompt text must be literal delegated context.",
2004
+ rationale: "Subagent prompts should not mutate the parent .omx/ultragoal ledger.",
2005
+ title: "Subagent should not add this",
2006
+ objective: "This must remain literal prompt text.",
2007
+ })}`,
2008
+ }, { cwd });
2009
+ assert.equal(result.outputJson, null);
2010
+ const plan = await readUltragoalPlan(cwd);
2011
+ assert.equal(plan.goals.length, 1);
2012
+ const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
2013
+ assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 0);
2014
+ assert.equal((ledger.match(/"event":"steering_rejected"/g) ?? []).length, 0);
2015
+ }
2016
+ finally {
2017
+ await rm(cwd, { recursive: true, force: true });
2018
+ }
2019
+ });
2020
+ it("dedupes repeated UserPromptSubmit ultragoal steering directives by prompt signature", async () => {
2021
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-dedupe-"));
2022
+ try {
2023
+ await createUltragoalPlan(cwd, {
2024
+ brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal dedupe fixture",
2025
+ goals: [{ title: "First", objective: "Complete first milestone with tests." }],
2026
+ });
2027
+ const prompt = `\`\`\`omx-ultragoal-steer
2028
+ ${JSON.stringify({
2029
+ kind: "add_subgoal",
2030
+ source: "user_prompt_submit",
2031
+ evidence: "Structured prompt-submit directive adds exactly one deduped goal.",
2032
+ rationale: "Use idempotent bridge semantics for repeated hook delivery.",
2033
+ title: "Deduped bridge regression",
2034
+ objective: "Verify repeated UserPromptSubmit steering does not duplicate goals.",
2035
+ })}
2036
+ \`\`\``;
2037
+ await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
2038
+ const second = await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
2039
+ const message = String(second.outputJson?.hookSpecificOutput?.additionalContext || "");
2040
+ assert.match(message, /deduped/);
2041
+ const plan = await readUltragoalPlan(cwd);
2042
+ assert.equal(plan.goals.filter((goal) => goal.title === "Deduped bridge regression").length, 1);
2043
+ const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
2044
+ assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 1);
2045
+ }
2046
+ finally {
2047
+ await rm(cwd, { recursive: true, force: true });
2048
+ }
2049
+ });
2050
+ it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
2051
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
2052
+ try {
2053
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2054
+ const result = await dispatchCodexNativeHook({
2055
+ hook_event_name: "UserPromptSubmit",
2056
+ cwd,
2057
+ session_id: "sess-ulw-ko",
2058
+ thread_id: "thread-ulw-ko",
2059
+ turn_id: "turn-ulw-ko",
2060
+ prompt: "ㅕㅣㅈ로 병렬 처리해줘",
2061
+ }, { cwd });
2062
+ assert.equal(result.omxEventName, "keyword-detector");
2063
+ assert.equal(result.skillState?.skill, "ultrawork");
2064
+ assert.equal(result.skillState?.keyword, "ulw");
2065
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2066
+ assert.match(additionalContext, /workflow keyword \"ulw\" -> ultrawork/);
2067
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ulw-ko", "ultrawork-state.json")), true);
2068
+ }
2069
+ finally {
2070
+ await rm(cwd, { recursive: true, force: true });
2071
+ }
2072
+ });
2073
+ it("adds ultrawork-specific activation guidance only for true ultrawork workflow activation", async () => {
2074
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultrawork-routing-"));
2075
+ try {
2076
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2077
+ const result = await dispatchCodexNativeHook({
2078
+ hook_event_name: "UserPromptSubmit",
2079
+ cwd,
2080
+ session_id: "sess-ultrawork-msg",
2081
+ thread_id: "thread-ultrawork-msg",
2082
+ turn_id: "turn-ultrawork-msg",
2083
+ prompt: "$ultrawork fan out the regression checks",
2084
+ }, { cwd });
2085
+ assert.equal(result.omxEventName, "keyword-detector");
2086
+ assert.equal(result.skillState?.skill, "ultrawork");
1558
2087
  const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
1559
2088
  assert.match(message, /\$ultrawork" -> ultrawork/);
1560
2089
  assert.match(message, /ground the task before editing/i);
@@ -2370,6 +2899,44 @@ exit 0
2370
2899
  await rm(cwd, { recursive: true, force: true });
2371
2900
  }
2372
2901
  });
2902
+ it("does not block Bash commands that only mention omx question in quoted arguments", async () => {
2903
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-quoted-mention-"));
2904
+ try {
2905
+ const result = await dispatchCodexNativeHook({
2906
+ hook_event_name: "PreToolUse",
2907
+ cwd,
2908
+ tool_name: "Bash",
2909
+ tool_use_id: "tool-question-quoted-mention",
2910
+ tool_input: {
2911
+ command: `omx ultragoal create-goals --brief "Deep interview says omx question failed in tmux"`,
2912
+ },
2913
+ }, { cwd });
2914
+ assert.equal(result.omxEventName, "pre-tool-use");
2915
+ assert.equal(result.outputJson, null);
2916
+ }
2917
+ finally {
2918
+ await rm(cwd, { recursive: true, force: true });
2919
+ }
2920
+ });
2921
+ it("does not block Bash heredocs that only document omx question text", async () => {
2922
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-heredoc-mention-"));
2923
+ try {
2924
+ const result = await dispatchCodexNativeHook({
2925
+ hook_event_name: "PreToolUse",
2926
+ cwd,
2927
+ tool_name: "Bash",
2928
+ tool_use_id: "tool-question-heredoc-mention",
2929
+ tool_input: {
2930
+ command: `cat > issue-notes.md <<'EOF'\nomx question failed in the attached tmux pane\nEOF`,
2931
+ },
2932
+ }, { cwd });
2933
+ assert.equal(result.omxEventName, "pre-tool-use");
2934
+ assert.equal(result.outputJson, null);
2935
+ }
2936
+ finally {
2937
+ await rm(cwd, { recursive: true, force: true });
2938
+ }
2939
+ });
2373
2940
  it("allows Bash omx question when the command preserves the leader-pane return hint", async () => {
2374
2941
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-allow-"));
2375
2942
  try {
@@ -3010,7 +3577,7 @@ exit 0
3010
3577
  cwd,
3011
3578
  tool_name: "Bash",
3012
3579
  tool_use_id: "tool-slop-git-priority",
3013
- tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
3580
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "quick hack fallback if it fails"' },
3014
3581
  }, { cwd });
3015
3582
  assert.equal(result.omxEventName, "pre-tool-use");
3016
3583
  assert.equal(result.outputJson?.decision, "block");
@@ -3029,7 +3596,7 @@ exit 0
3029
3596
  cwd,
3030
3597
  tool_name: "Bash",
3031
3598
  tool_use_id: "tool-git-commit-invalid",
3032
- tool_input: { command: 'git commit -m "fix tests"' },
3599
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
3033
3600
  }, { cwd });
3034
3601
  assert.equal(result.omxEventName, "pre-tool-use");
3035
3602
  assert.deepEqual(result.outputJson, {
@@ -3054,11 +3621,35 @@ exit 0
3054
3621
  await rm(cwd, { recursive: true, force: true });
3055
3622
  }
3056
3623
  });
3057
- it("allows non-Lore git commit messages when the Lore commit guard is explicitly disabled", async () => {
3624
+ it("blocks PreToolUse git commit when process env explicitly enables the Lore commit guard", async () => {
3625
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-enabled-"));
3626
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3627
+ try {
3628
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3629
+ const result = await dispatchCodexNativeHook({
3630
+ hook_event_name: "PreToolUse",
3631
+ cwd,
3632
+ tool_name: "Bash",
3633
+ tool_use_id: "tool-git-commit-lore-env-enabled",
3634
+ tool_input: { command: 'git commit -m "fix tests"' },
3635
+ }, { cwd });
3636
+ assert.equal(result.omxEventName, "pre-tool-use");
3637
+ assert.equal(result.outputJson?.decision, "block");
3638
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3639
+ }
3640
+ finally {
3641
+ if (original === undefined)
3642
+ delete process.env.OMX_LORE_COMMIT_GUARD;
3643
+ else
3644
+ process.env.OMX_LORE_COMMIT_GUARD = original;
3645
+ await rm(cwd, { recursive: true, force: true });
3646
+ }
3647
+ });
3648
+ it("allows non-Lore git commit messages when the Lore commit guard is disabled by default", async () => {
3058
3649
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
3059
3650
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3060
3651
  try {
3061
- process.env.OMX_LORE_COMMIT_GUARD = "0";
3652
+ delete process.env.OMX_LORE_COMMIT_GUARD;
3062
3653
  const result = await dispatchCodexNativeHook({
3063
3654
  hook_event_name: "PreToolUse",
3064
3655
  cwd,
@@ -3077,6 +3668,60 @@ exit 0
3077
3668
  await rm(cwd, { recursive: true, force: true });
3078
3669
  }
3079
3670
  });
3671
+ it("blocks non-Lore git commit messages when the Lore commit guard is enabled in CODEX_HOME config.toml", async () => {
3672
+ await withLoreGuardConfig("1", "config-enabled", async (cwd) => {
3673
+ const result = await dispatchCodexNativeHook({
3674
+ hook_event_name: "PreToolUse",
3675
+ cwd,
3676
+ tool_name: "Bash",
3677
+ tool_use_id: "tool-git-commit-lore-config-enabled",
3678
+ tool_input: { command: 'git commit -m "fix: conventional"' },
3679
+ }, { cwd });
3680
+ assert.equal(result.omxEventName, "pre-tool-use");
3681
+ assert.equal(result.outputJson?.decision, "block");
3682
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3683
+ });
3684
+ });
3685
+ it("allows non-Lore git commit messages when the Lore commit guard is disabled in CODEX_HOME config.toml", async () => {
3686
+ await withLoreGuardConfig("0", "config-disabled", async (cwd) => {
3687
+ const result = await dispatchCodexNativeHook({
3688
+ hook_event_name: "PreToolUse",
3689
+ cwd,
3690
+ tool_name: "Bash",
3691
+ tool_use_id: "tool-git-commit-lore-config-disabled",
3692
+ tool_input: { command: 'git commit -m "fix: use conventional commit"' },
3693
+ }, { cwd });
3694
+ assert.equal(result.omxEventName, "pre-tool-use");
3695
+ assert.equal(result.outputJson, null);
3696
+ });
3697
+ });
3698
+ it("lets inline Lore commit guard values override a disabled CODEX_HOME config.toml", async () => {
3699
+ await withLoreGuardConfig("0", "config-inline-enabled", async (cwd) => {
3700
+ const result = await dispatchCodexNativeHook({
3701
+ hook_event_name: "PreToolUse",
3702
+ cwd,
3703
+ tool_name: "Bash",
3704
+ tool_use_id: "tool-git-commit-lore-config-inline-enabled",
3705
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix: conventional"' },
3706
+ }, { cwd });
3707
+ assert.equal(result.omxEventName, "pre-tool-use");
3708
+ assert.equal(result.outputJson?.decision, "block");
3709
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3710
+ });
3711
+ });
3712
+ it("restores default-off Lore guard when env -u removes a disabled CODEX_HOME config source", async () => {
3713
+ await withLoreGuardConfig("0", "config-codex-home-unset", async (cwd) => {
3714
+ const result = await dispatchCodexNativeHook({
3715
+ hook_event_name: "PreToolUse",
3716
+ cwd,
3717
+ tool_name: "Bash",
3718
+ tool_use_id: "tool-git-commit-lore-config-codex-home-unset",
3719
+ tool_input: { command: 'env -u CODEX_HOME git commit -m "fix: conventional"' },
3720
+ }, { cwd });
3721
+ assert.equal(result.omxEventName, "pre-tool-use");
3722
+ assert.equal(result.outputJson, null);
3723
+ });
3724
+ });
3080
3725
  it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
3081
3726
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
3082
3727
  try {
@@ -3094,7 +3739,30 @@ exit 0
3094
3739
  await rm(cwd, { recursive: true, force: true });
3095
3740
  }
3096
3741
  });
3097
- it("does not treat newline-separated Lore guard assignment as inline git commit env", async () => {
3742
+ it("allows inline disabled guard to override an enabled process env", async () => {
3743
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-override-disabled-"));
3744
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3745
+ try {
3746
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3747
+ const result = await dispatchCodexNativeHook({
3748
+ hook_event_name: "PreToolUse",
3749
+ cwd,
3750
+ tool_name: "Bash",
3751
+ tool_use_id: "tool-git-commit-lore-inline-override-disabled",
3752
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
3753
+ }, { cwd });
3754
+ assert.equal(result.omxEventName, "pre-tool-use");
3755
+ assert.equal(result.outputJson, null);
3756
+ }
3757
+ finally {
3758
+ if (original === undefined)
3759
+ delete process.env.OMX_LORE_COMMIT_GUARD;
3760
+ else
3761
+ process.env.OMX_LORE_COMMIT_GUARD = original;
3762
+ await rm(cwd, { recursive: true, force: true });
3763
+ }
3764
+ });
3765
+ it("does not treat newline-separated Lore guard assignment as inline git commit opt-in", async () => {
3098
3766
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
3099
3767
  try {
3100
3768
  const result = await dispatchCodexNativeHook({
@@ -3102,21 +3770,33 @@ exit 0
3102
3770
  cwd,
3103
3771
  tool_name: "Bash",
3104
3772
  tool_use_id: "tool-git-commit-lore-newline-assignment",
3105
- tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0\ngit commit -m "fix: conventional"' },
3773
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1\ngit commit -m "fix: conventional"' },
3106
3774
  }, { cwd });
3107
3775
  assert.equal(result.omxEventName, "pre-tool-use");
3108
- assert.equal(result.outputJson?.decision, "block");
3109
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3776
+ assert.equal(result.outputJson, null);
3110
3777
  }
3111
3778
  finally {
3112
3779
  await rm(cwd, { recursive: true, force: true });
3113
3780
  }
3114
3781
  });
3115
- it("restores default-on Lore guard when env -u unsets a disabled process env", async () => {
3782
+ it("restores default-off Lore guard when env -u unsets a config.toml fallback", async () => {
3783
+ await withLoreGuardConfig("1", "config-env-unset", async (cwd) => {
3784
+ const result = await dispatchCodexNativeHook({
3785
+ hook_event_name: "PreToolUse",
3786
+ cwd,
3787
+ tool_name: "Bash",
3788
+ tool_use_id: "tool-git-commit-lore-config-env-unset",
3789
+ tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
3790
+ }, { cwd });
3791
+ assert.equal(result.omxEventName, "pre-tool-use");
3792
+ assert.equal(result.outputJson, null);
3793
+ });
3794
+ });
3795
+ it("restores default-off Lore guard when env -u unsets an enabled process env", async () => {
3116
3796
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
3117
3797
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3118
3798
  try {
3119
- process.env.OMX_LORE_COMMIT_GUARD = "0";
3799
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3120
3800
  const result = await dispatchCodexNativeHook({
3121
3801
  hook_event_name: "PreToolUse",
3122
3802
  cwd,
@@ -3125,8 +3805,7 @@ exit 0
3125
3805
  tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
3126
3806
  }, { cwd });
3127
3807
  assert.equal(result.omxEventName, "pre-tool-use");
3128
- assert.equal(result.outputJson?.decision, "block");
3129
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3808
+ assert.equal(result.outputJson, null);
3130
3809
  }
3131
3810
  finally {
3132
3811
  if (original === undefined)
@@ -3136,11 +3815,11 @@ exit 0
3136
3815
  await rm(cwd, { recursive: true, force: true });
3137
3816
  }
3138
3817
  });
3139
- it("restores default-on Lore guard when env -i clears a disabled process env", async () => {
3818
+ it("restores default-off Lore guard when env -i clears an enabled process env", async () => {
3140
3819
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
3141
3820
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3142
3821
  try {
3143
- process.env.OMX_LORE_COMMIT_GUARD = "0";
3822
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3144
3823
  const result = await dispatchCodexNativeHook({
3145
3824
  hook_event_name: "PreToolUse",
3146
3825
  cwd,
@@ -3149,8 +3828,7 @@ exit 0
3149
3828
  tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
3150
3829
  }, { cwd });
3151
3830
  assert.equal(result.omxEventName, "pre-tool-use");
3152
- assert.equal(result.outputJson?.decision, "block");
3153
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3831
+ assert.equal(result.outputJson, null);
3154
3832
  }
3155
3833
  finally {
3156
3834
  if (original === undefined)
@@ -3160,7 +3838,7 @@ exit 0
3160
3838
  await rm(cwd, { recursive: true, force: true });
3161
3839
  }
3162
3840
  });
3163
- it("keeps Lore commit enforcement enabled for unknown inline guard values", async () => {
3841
+ it("keeps Lore commit enforcement disabled for unknown inline guard values", async () => {
3164
3842
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
3165
3843
  try {
3166
3844
  const result = await dispatchCodexNativeHook({
@@ -3171,8 +3849,7 @@ exit 0
3171
3849
  tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
3172
3850
  }, { cwd });
3173
3851
  assert.equal(result.omxEventName, "pre-tool-use");
3174
- assert.equal(result.outputJson?.decision, "block");
3175
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3852
+ assert.equal(result.outputJson, null);
3176
3853
  }
3177
3854
  finally {
3178
3855
  await rm(cwd, { recursive: true, force: true });
@@ -3201,7 +3878,7 @@ exit 0
3201
3878
  await rm(cwd, { recursive: true, force: true });
3202
3879
  }
3203
3880
  });
3204
- it("keeps Lore commit enforcement enabled for unknown guard values", async () => {
3881
+ it("keeps Lore commit enforcement disabled for unknown guard values", async () => {
3205
3882
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
3206
3883
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3207
3884
  try {
@@ -3214,8 +3891,7 @@ exit 0
3214
3891
  tool_input: { command: 'git commit -m "fix tests"' },
3215
3892
  }, { cwd });
3216
3893
  assert.equal(result.omxEventName, "pre-tool-use");
3217
- assert.equal(result.outputJson?.decision, "block");
3218
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3894
+ assert.equal(result.outputJson, null);
3219
3895
  }
3220
3896
  finally {
3221
3897
  if (original === undefined)
@@ -3308,7 +3984,7 @@ exit 0
3308
3984
  cwd,
3309
3985
  tool_name: "Bash",
3310
3986
  tool_use_id: "tool-git-commit-env-invalid",
3311
- tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
3987
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 HUSKY=0 git commit -m "fix tests"' },
3312
3988
  }, { cwd });
3313
3989
  assert.equal(result.omxEventName, "pre-tool-use");
3314
3990
  assert.deepEqual(result.outputJson, {
@@ -3338,7 +4014,7 @@ exit 0
3338
4014
  cwd,
3339
4015
  tool_name: "Bash",
3340
4016
  tool_use_id: "tool-git-commit-option-invalid",
3341
- tool_input: { command: 'git -c core.editor=true commit -m "fix tests"' },
4017
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git -c core.editor=true commit -m "fix tests"' },
3342
4018
  }, { cwd });
3343
4019
  assert.equal(result.omxEventName, "pre-tool-use");
3344
4020
  assert.deepEqual(result.outputJson, {
@@ -3368,7 +4044,7 @@ exit 0
3368
4044
  cwd,
3369
4045
  tool_name: "Bash",
3370
4046
  tool_use_id: "tool-git-exe-commit-env-wrapper-invalid",
3371
- tool_input: { command: 'env git.exe commit -m "fix tests"' },
4047
+ tool_input: { command: 'env OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3372
4048
  }, { cwd });
3373
4049
  assert.equal(result.omxEventName, "pre-tool-use");
3374
4050
  assert.deepEqual(result.outputJson, {
@@ -3398,7 +4074,7 @@ exit 0
3398
4074
  cwd,
3399
4075
  tool_name: "Bash",
3400
4076
  tool_use_id: "tool-git-exe-commit-invalid",
3401
- tool_input: { command: 'git.exe commit -m "fix tests"' },
4077
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3402
4078
  }, { cwd });
3403
4079
  assert.equal(result.omxEventName, "pre-tool-use");
3404
4080
  assert.deepEqual(result.outputJson, {
@@ -3428,7 +4104,7 @@ exit 0
3428
4104
  cwd,
3429
4105
  tool_name: "Bash",
3430
4106
  tool_use_id: "tool-git-exe-commit-env-flag-wrapper-invalid",
3431
- tool_input: { command: 'env -i PATH=/usr/bin git.exe commit -m "fix tests"' },
4107
+ tool_input: { command: 'env -i PATH=/usr/bin OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3432
4108
  }, { cwd });
3433
4109
  assert.equal(result.omxEventName, "pre-tool-use");
3434
4110
  assert.deepEqual(result.outputJson, {
@@ -3458,7 +4134,7 @@ exit 0
3458
4134
  cwd,
3459
4135
  tool_name: "Bash",
3460
4136
  tool_use_id: "tool-git-exe-commit-env-value-wrapper-invalid",
3461
- tool_input: { command: 'env -u FOO git.exe commit -m "fix tests"' },
4137
+ tool_input: { command: 'env -u FOO OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3462
4138
  }, { cwd });
3463
4139
  assert.equal(result.omxEventName, "pre-tool-use");
3464
4140
  assert.deepEqual(result.outputJson, {
@@ -3488,7 +4164,7 @@ exit 0
3488
4164
  cwd,
3489
4165
  tool_name: "Bash",
3490
4166
  tool_use_id: "tool-git-exe-commit-windows-path-invalid",
3491
- tool_input: { command: '"C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
4167
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
3492
4168
  }, { cwd });
3493
4169
  assert.equal(result.omxEventName, "pre-tool-use");
3494
4170
  assert.deepEqual(result.outputJson, {
@@ -3518,7 +4194,7 @@ exit 0
3518
4194
  cwd,
3519
4195
  tool_name: "Bash",
3520
4196
  tool_use_id: "tool-git-exe-commit-windows-backslash-path-invalid",
3521
- tool_input: { command: '"C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
4197
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
3522
4198
  }, { cwd });
3523
4199
  assert.equal(result.omxEventName, "pre-tool-use");
3524
4200
  assert.deepEqual(result.outputJson, {
@@ -3548,7 +4224,7 @@ exit 0
3548
4224
  cwd,
3549
4225
  tool_name: "Bash",
3550
4226
  tool_use_id: "tool-git-commit-path-invalid",
3551
- tool_input: { command: '/usr/bin/git commit -m "fix tests"' },
4227
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 /usr/bin/git commit -m "fix tests"' },
3552
4228
  }, { cwd });
3553
4229
  assert.equal(result.omxEventName, "pre-tool-use");
3554
4230
  assert.deepEqual(result.outputJson, {
@@ -3578,7 +4254,7 @@ exit 0
3578
4254
  cwd,
3579
4255
  tool_name: "Bash",
3580
4256
  tool_use_id: "tool-git-commit-file",
3581
- tool_input: { command: "git commit -F .git/COMMIT_EDITMSG" },
4257
+ tool_input: { command: "OMX_LORE_COMMIT_GUARD=1 git commit -F .git/COMMIT_EDITMSG" },
3582
4258
  }, { cwd });
3583
4259
  assert.equal(result.omxEventName, "pre-tool-use");
3584
4260
  assert.deepEqual(result.outputJson, {
@@ -3607,7 +4283,7 @@ exit 0
3607
4283
  tool_use_id: "tool-git-commit-missing-omx-coauthor",
3608
4284
  tool_input: {
3609
4285
  command: [
3610
- 'git commit',
4286
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3611
4287
  '-m "Prevent invalid history from bypassing Lore enforcement"',
3612
4288
  '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
3613
4289
  '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
@@ -3642,7 +4318,7 @@ exit 0
3642
4318
  tool_use_id: "tool-git-commit-valid",
3643
4319
  tool_input: {
3644
4320
  command: [
3645
- 'git commit',
4321
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3646
4322
  '-m "Prevent invalid history from bypassing Lore enforcement"',
3647
4323
  '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
3648
4324
  '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
@@ -3668,7 +4344,7 @@ exit 0
3668
4344
  tool_use_id: "tool-git-commit-compact-coauthor",
3669
4345
  tool_input: {
3670
4346
  command: [
3671
- 'git commit',
4347
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3672
4348
  '-m "Launch lvisai.xyz intro site"',
3673
4349
  '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
3674
4350
  ].join(" "),
@@ -3691,7 +4367,7 @@ exit 0
3691
4367
  tool_use_id: "tool-git-commit-compact-trailers",
3692
4368
  tool_input: {
3693
4369
  command: [
3694
- 'git commit',
4370
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3695
4371
  '-m "Launch lvisai.xyz intro site"',
3696
4372
  '-m "Constraint: Native PreToolUse can only inspect inline Bash command text\nTested: node --test dist/scripts/__tests__/codex-native-hook.test.js\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>"',
3697
4373
  ].join(" "),
@@ -3714,7 +4390,7 @@ exit 0
3714
4390
  tool_use_id: "tool-git-commit-compact-no-separator",
3715
4391
  tool_input: {
3716
4392
  command: [
3717
- 'git commit',
4393
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3718
4394
  '--message="Launch lvisai.xyz intro site\nCo-authored-by: OmX <omx@oh-my-codex.dev>"',
3719
4395
  ].join(" "),
3720
4396
  },
@@ -4052,41 +4728,36 @@ exit 0
4052
4728
  await rm(cwd, { recursive: true, force: true });
4053
4729
  }
4054
4730
  });
4055
- it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
4056
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
4731
+ it("does not treat non-MCP source output containing detector constants as MCP transport death", async () => {
4732
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-read-mcp-source-"));
4057
4733
  try {
4058
4734
  const result = await dispatchCodexNativeHook({
4059
4735
  hook_event_name: "PostToolUse",
4060
4736
  cwd,
4061
- tool_name: "mcp__omx_state__state_write",
4062
- tool_use_id: "tool-mcp-transport",
4063
- tool_input: { mode: "team", active: true },
4064
- tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
4737
+ tool_name: "Read",
4738
+ tool_use_id: "tool-read-mcp-source",
4739
+ tool_input: { file_path: "src/scripts/codex-native-pre-post.ts" },
4740
+ tool_response: "const MCP_TRANSPORT_FAILURE_PATTERNS = [/transport closed/i, /server disconnected/i];\nconst context = /\\bmcp\\b/i;",
4065
4741
  }, { cwd });
4066
4742
  assert.equal(result.omxEventName, "post-tool-use");
4067
- const output = result.outputJson;
4068
- assert.equal(output?.decision, "block");
4069
- assert.equal(output?.reason, "The MCP tool appears to have lost its transport/server connection. Preserve state, debug the transport failure, and use OMX CLI/file-backed fallbacks instead of retrying blindly.");
4070
- const additionalContext = String(output?.hookSpecificOutput?.additionalContext ?? "");
4071
- assert.match(additionalContext, /omx state write --input/);
4072
- assert.match(additionalContext, /plain Node stdio processes/i);
4073
- assert.match(additionalContext, /read-stall-state/);
4074
- assert.match(additionalContext, /OMX_MCP_TRANSPORT_DEBUG=1/);
4743
+ assert.equal(result.outputJson, null);
4075
4744
  }
4076
4745
  finally {
4077
4746
  await rm(cwd, { recursive: true, force: true });
4078
4747
  }
4079
4748
  });
4080
- it("does not classify non-transport MCP failures as transport death", async () => {
4081
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-nontransport-"));
4749
+ it("does not treat non-MCP docs stdout mentioning closed MCP transport as transport death", async () => {
4750
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-docs-mcp-log-"));
4082
4751
  try {
4083
4752
  const result = await dispatchCodexNativeHook({
4084
4753
  hook_event_name: "PostToolUse",
4085
4754
  cwd,
4086
- tool_name: "mcp__omx_state__state_write",
4087
- tool_use_id: "tool-mcp-nontransport",
4088
- tool_input: { active: true },
4089
- tool_response: "{\"error\":\"validation failed\",\"details\":\"mode is required\"}",
4755
+ tool_name: "ShellOutput",
4756
+ tool_use_id: "tool-docs-mcp-log",
4757
+ tool_response: JSON.stringify({
4758
+ stdout: "Troubleshooting note: MCP transport closed after the server disconnected in an old log.",
4759
+ stderr: "",
4760
+ }),
4090
4761
  }, { cwd });
4091
4762
  assert.equal(result.omxEventName, "post-tool-use");
4092
4763
  assert.equal(result.outputJson, null);
@@ -4095,9 +4766,90 @@ exit 0
4095
4766
  await rm(cwd, { recursive: true, force: true });
4096
4767
  }
4097
4768
  });
4098
- it("marks active team state failed on MCP transport death without deleting team state", async () => {
4099
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-mcp-transport-"));
4100
- const previousCwd = process.cwd();
4769
+ it("does not MCP-block non-MCP command output with unrelated stderr and MCP transport stdout", async () => {
4770
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-nonmcp-mixed-output-"));
4771
+ try {
4772
+ const result = await dispatchCodexNativeHook({
4773
+ hook_event_name: "PostToolUse",
4774
+ cwd,
4775
+ tool_name: "ShellOutput",
4776
+ tool_use_id: "tool-nonmcp-mixed-output",
4777
+ tool_response: JSON.stringify({
4778
+ stdout: "captured log line: MCP transport closed before response",
4779
+ stderr: "grep: fixture.txt: No such file or directory",
4780
+ }),
4781
+ }, { cwd });
4782
+ assert.equal(result.omxEventName, "post-tool-use");
4783
+ assert.equal(result.outputJson, null);
4784
+ }
4785
+ finally {
4786
+ await rm(cwd, { recursive: true, force: true });
4787
+ }
4788
+ });
4789
+ it("still blocks MCP-like raw transport failures", async () => {
4790
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-raw-transport-"));
4791
+ try {
4792
+ const result = await dispatchCodexNativeHook({
4793
+ hook_event_name: "PostToolUse",
4794
+ cwd,
4795
+ tool_name: "mcp__omx_state__state_write",
4796
+ tool_use_id: "tool-mcp-raw-transport",
4797
+ tool_response: "transport closed after server disconnected",
4798
+ }, { cwd });
4799
+ assert.equal(result.omxEventName, "post-tool-use");
4800
+ assert.equal(result.outputJson?.decision, "block");
4801
+ assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
4802
+ }
4803
+ finally {
4804
+ await rm(cwd, { recursive: true, force: true });
4805
+ }
4806
+ });
4807
+ it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
4808
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
4809
+ try {
4810
+ const result = await dispatchCodexNativeHook({
4811
+ hook_event_name: "PostToolUse",
4812
+ cwd,
4813
+ tool_name: "mcp__omx_state__state_write",
4814
+ tool_use_id: "tool-mcp-transport",
4815
+ tool_input: { mode: "team", active: true },
4816
+ tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
4817
+ }, { cwd });
4818
+ assert.equal(result.omxEventName, "post-tool-use");
4819
+ const output = result.outputJson;
4820
+ assert.equal(output?.decision, "block");
4821
+ assert.equal(output?.reason, "The MCP tool appears to have lost its transport/server connection. Preserve state, debug the transport failure, and use OMX CLI/file-backed fallbacks instead of retrying blindly.");
4822
+ const additionalContext = String(output?.hookSpecificOutput?.additionalContext ?? "");
4823
+ assert.match(additionalContext, /omx state write --input/);
4824
+ assert.match(additionalContext, /plain Node stdio processes/i);
4825
+ assert.match(additionalContext, /read-stall-state/);
4826
+ assert.match(additionalContext, /OMX_MCP_TRANSPORT_DEBUG=1/);
4827
+ }
4828
+ finally {
4829
+ await rm(cwd, { recursive: true, force: true });
4830
+ }
4831
+ });
4832
+ it("does not classify non-transport MCP failures as transport death", async () => {
4833
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-nontransport-"));
4834
+ try {
4835
+ const result = await dispatchCodexNativeHook({
4836
+ hook_event_name: "PostToolUse",
4837
+ cwd,
4838
+ tool_name: "mcp__omx_state__state_write",
4839
+ tool_use_id: "tool-mcp-nontransport",
4840
+ tool_input: { active: true },
4841
+ tool_response: "{\"error\":\"validation failed\",\"details\":\"mode is required\"}",
4842
+ }, { cwd });
4843
+ assert.equal(result.omxEventName, "post-tool-use");
4844
+ assert.equal(result.outputJson, null);
4845
+ }
4846
+ finally {
4847
+ await rm(cwd, { recursive: true, force: true });
4848
+ }
4849
+ });
4850
+ it("marks active team state failed on MCP transport death without deleting team state", async () => {
4851
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-mcp-transport-"));
4852
+ const previousCwd = process.cwd();
4101
4853
  try {
4102
4854
  process.chdir(cwd);
4103
4855
  await initTeamState("transport-team", "task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-transport" });
@@ -4366,6 +5118,51 @@ exit 0
4366
5118
  await rm(cwd, { recursive: true, force: true });
4367
5119
  }
4368
5120
  });
5121
+ it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
5122
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
5123
+ try {
5124
+ const stateDir = join(cwd, ".omx", "state");
5125
+ const sessionId = "sess-stop-autopilot-review-compact";
5126
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5127
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
5128
+ active: true,
5129
+ mode: "autopilot",
5130
+ current_phase: "code-review",
5131
+ state: {
5132
+ phase_cycle: ["ralplan", "ralph", "code-review"],
5133
+ handoff_artifacts: {
5134
+ ralplan: ".omx/plans/prd-issue-2366.md",
5135
+ ralph: { verification: ["npm test"] },
5136
+ code_review: null,
5137
+ },
5138
+ review_verdict: null,
5139
+ },
5140
+ });
5141
+ const compactBoundary = await dispatchCodexNativeHook({
5142
+ hook_event_name: "Stop",
5143
+ cwd,
5144
+ session_id: sessionId,
5145
+ stop_reason: "context compact",
5146
+ }, { cwd });
5147
+ const resumedStop = await dispatchCodexNativeHook({
5148
+ hook_event_name: "Stop",
5149
+ cwd,
5150
+ session_id: sessionId,
5151
+ }, { cwd });
5152
+ assert.equal(compactBoundary.omxEventName, "stop");
5153
+ assert.equal(compactBoundary.outputJson, null);
5154
+ assert.equal(resumedStop.omxEventName, "stop");
5155
+ assert.deepEqual(resumedStop.outputJson, {
5156
+ decision: "block",
5157
+ reason: "OMX autopilot is still active (phase: code-review); continue the task and gather fresh verification evidence before stopping.",
5158
+ stopReason: "autopilot_code-review",
5159
+ systemMessage: "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state.",
5160
+ });
5161
+ }
5162
+ finally {
5163
+ await rm(cwd, { recursive: true, force: true });
5164
+ }
5165
+ });
4369
5166
  it("suppresses duplicate Autopilot planning Stop replays so stale planning state cannot loop indefinitely", async () => {
4370
5167
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
4371
5168
  try {
@@ -4402,6 +5199,80 @@ exit 0
4402
5199
  await rm(cwd, { recursive: true, force: true });
4403
5200
  }
4404
5201
  });
5202
+ it("allows Stop when terminal Autopilot run-state shadows stale session ralplan state", async () => {
5203
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-terminal-run-state-"));
5204
+ try {
5205
+ const stateDir = join(cwd, ".omx", "state");
5206
+ const sessionId = "sess-stop-autopilot-terminal-run-state";
5207
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5208
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
5209
+ active: true,
5210
+ mode: "autopilot",
5211
+ current_phase: "ralplan",
5212
+ });
5213
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
5214
+ version: 1,
5215
+ active: false,
5216
+ mode: "autopilot",
5217
+ outcome: "finish",
5218
+ lifecycle_outcome: "finished",
5219
+ current_phase: "complete",
5220
+ completed_at: "2026-05-20T11:00:00.000Z",
5221
+ updated_at: "2026-05-20T11:00:00.000Z",
5222
+ });
5223
+ const result = await dispatchCodexNativeHook({
5224
+ hook_event_name: "Stop",
5225
+ cwd,
5226
+ session_id: sessionId,
5227
+ thread_id: "thread-stop-autopilot-terminal-run-state",
5228
+ turn_id: "turn-stop-autopilot-terminal-run-state-1",
5229
+ last_assistant_message: "Done. Verification passed.",
5230
+ }, { cwd });
5231
+ assert.equal(result.omxEventName, "stop");
5232
+ assert.equal(result.outputJson, null);
5233
+ }
5234
+ finally {
5235
+ await rm(cwd, { recursive: true, force: true });
5236
+ }
5237
+ });
5238
+ it("still blocks Stop while Autopilot ralplan state is genuinely non-terminal", async () => {
5239
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-active-ralplan-"));
5240
+ try {
5241
+ const stateDir = join(cwd, ".omx", "state");
5242
+ const sessionId = "sess-stop-autopilot-active-ralplan";
5243
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5244
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
5245
+ active: true,
5246
+ mode: "autopilot",
5247
+ current_phase: "ralplan",
5248
+ });
5249
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
5250
+ version: 1,
5251
+ active: true,
5252
+ mode: "autopilot",
5253
+ outcome: "continue",
5254
+ current_phase: "ralplan",
5255
+ updated_at: "2026-05-20T11:00:00.000Z",
5256
+ });
5257
+ const result = await dispatchCodexNativeHook({
5258
+ hook_event_name: "Stop",
5259
+ cwd,
5260
+ session_id: sessionId,
5261
+ thread_id: "thread-stop-autopilot-active-ralplan",
5262
+ turn_id: "turn-stop-autopilot-active-ralplan-1",
5263
+ }, { cwd });
5264
+ assert.equal(result.omxEventName, "stop");
5265
+ assert.deepEqual(result.outputJson, {
5266
+ decision: "block",
5267
+ reason: "OMX autopilot is still active (phase: ralplan); continue the task and gather fresh verification evidence before stopping.",
5268
+ stopReason: "autopilot_ralplan",
5269
+ systemMessage: "OMX autopilot is still active (phase: ralplan).",
5270
+ });
5271
+ }
5272
+ finally {
5273
+ await rm(cwd, { recursive: true, force: true });
5274
+ }
5275
+ });
4405
5276
  it("does not block Stop from stale root Autopilot planning state when the explicit session has no scoped state", async () => {
4406
5277
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autopilot-planning-"));
4407
5278
  try {
@@ -4858,6 +5729,87 @@ exit 0
4858
5729
  await rm(cwd, { recursive: true, force: true });
4859
5730
  }
4860
5731
  });
5732
+ it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
5733
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
5734
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5735
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5736
+ const prevPath = process.env.PATH;
5737
+ try {
5738
+ await initTeamState("worker-stop-team-busy-leader", "worker stop busy leader", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-busy-leader" });
5739
+ const fakeBinDir = join(cwd, "fake-bin");
5740
+ const tmuxLogPath = join(cwd, "tmux.log");
5741
+ await mkdir(fakeBinDir, { recursive: true });
5742
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { busyLeader: true }));
5743
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5744
+ const stateDir = join(cwd, ".omx", "state");
5745
+ const teamDir = join(stateDir, "team", "worker-stop-team-busy-leader");
5746
+ const workerDir = join(teamDir, "workers", "worker-1");
5747
+ await writeJson(join(teamDir, "config.json"), {
5748
+ name: "worker-stop-team-busy-leader",
5749
+ tmux_session: "omx-team-worker-stop",
5750
+ leader_pane_id: "%42",
5751
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5752
+ });
5753
+ await writeJson(join(teamDir, "manifest.v2.json"), {
5754
+ name: "worker-stop-team-busy-leader",
5755
+ tmux_session: "omx-team-worker-stop",
5756
+ leader_pane_id: "%42",
5757
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5758
+ });
5759
+ await writeJson(join(workerDir, "identity.json"), {
5760
+ name: "worker-1",
5761
+ index: 1,
5762
+ role: "executor",
5763
+ assigned_tasks: ["1"],
5764
+ worktree_path: cwd,
5765
+ team_state_root: stateDir,
5766
+ });
5767
+ await writeJson(join(workerDir, "status.json"), {
5768
+ state: "done",
5769
+ current_task_id: "1",
5770
+ updated_at: new Date().toISOString(),
5771
+ });
5772
+ await writeJson(join(teamDir, "tasks", "task-1.json"), {
5773
+ id: "1",
5774
+ subject: "hook task",
5775
+ description: "finish hook task",
5776
+ status: "completed",
5777
+ owner: "worker-1",
5778
+ created_at: new Date().toISOString(),
5779
+ });
5780
+ process.env.OMX_TEAM_WORKER = "worker-stop-team-busy-leader/worker-1";
5781
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5782
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
5783
+ const result = await dispatchCodexNativeHook({
5784
+ hook_event_name: "Stop",
5785
+ cwd,
5786
+ session_id: "sess-stop-team-worker-busy-leader",
5787
+ }, { cwd });
5788
+ assert.equal(result.outputJson, null);
5789
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
5790
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
5791
+ assert.match(tmuxLog, /send-keys -t %42 Tab/);
5792
+ assert.match(tmuxLog, /send-keys -t %42 C-m/);
5793
+ assert.ok(tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"), "busy worker-stop nudge should press Tab before C-m");
5794
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
5795
+ assert.equal(nudgeState.delivery, "queued");
5796
+ }
5797
+ finally {
5798
+ if (typeof prevTeamWorker === "string")
5799
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
5800
+ else
5801
+ delete process.env.OMX_TEAM_WORKER;
5802
+ if (typeof prevTeamStateRoot === "string")
5803
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5804
+ else
5805
+ delete process.env.OMX_TEAM_STATE_ROOT;
5806
+ if (typeof prevPath === "string")
5807
+ process.env.PATH = prevPath;
5808
+ else
5809
+ delete process.env.PATH;
5810
+ await rm(cwd, { recursive: true, force: true });
5811
+ }
5812
+ });
4861
5813
  it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
4862
5814
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
4863
5815
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
@@ -5622,13 +6574,25 @@ exit 0
5622
6574
  await rm(cwd, { recursive: true, force: true });
5623
6575
  }
5624
6576
  });
5625
- it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
5626
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
6577
+ it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
6578
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
5627
6579
  try {
5628
6580
  const stateDir = join(cwd, ".omx", "state");
5629
- const sessionId = "sess-stop-terminal-ralplan";
6581
+ const sessionId = "sess-stop-stale-session-ralplan";
5630
6582
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5631
6583
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6584
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6585
+ active: false,
6586
+ skill: "ralplan",
6587
+ phase: "reviewing",
6588
+ active_skills: [],
6589
+ });
6590
+ await writeJson(join(stateDir, "ralplan-state.json"), {
6591
+ active: false,
6592
+ mode: "ralplan",
6593
+ current_phase: "complete",
6594
+ session_id: sessionId,
6595
+ });
5632
6596
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
5633
6597
  active: true,
5634
6598
  skill: "ralplan",
@@ -5647,16 +6611,6 @@ exit 0
5647
6611
  current_phase: "planning",
5648
6612
  session_id: sessionId,
5649
6613
  });
5650
- await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
5651
- version: 1,
5652
- mode: "ralplan",
5653
- active: false,
5654
- outcome: "finish",
5655
- lifecycle_outcome: "finished",
5656
- current_phase: "complete",
5657
- completed_at: "2026-05-01T00:00:00.000Z",
5658
- updated_at: "2026-05-01T00:00:00.000Z",
5659
- });
5660
6614
  const result = await dispatchCodexNativeHook({
5661
6615
  hook_event_name: "Stop",
5662
6616
  cwd,
@@ -5669,13 +6623,26 @@ exit 0
5669
6623
  await rm(cwd, { recursive: true, force: true });
5670
6624
  }
5671
6625
  });
5672
- it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
5673
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
6626
+ it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
6627
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
5674
6628
  try {
5675
6629
  const stateDir = join(cwd, ".omx", "state");
5676
- const sessionId = "sess-stop-current-ralplan";
6630
+ const sessionId = "sess-stop-current-active-ralplan";
5677
6631
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5678
6632
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6633
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6634
+ active: false,
6635
+ skill: "ralplan",
6636
+ phase: "complete",
6637
+ session_id: "sess-stop-old-ralplan",
6638
+ active_skills: [],
6639
+ });
6640
+ await writeJson(join(stateDir, "ralplan-state.json"), {
6641
+ active: false,
6642
+ mode: "ralplan",
6643
+ current_phase: "complete",
6644
+ session_id: "sess-stop-old-ralplan",
6645
+ });
5679
6646
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
5680
6647
  active: true,
5681
6648
  skill: "ralplan",
@@ -5692,7 +6659,7 @@ exit 0
5692
6659
  active: true,
5693
6660
  mode: "ralplan",
5694
6661
  current_phase: "planning",
5695
- session_id: "sess-other-ralplan",
6662
+ session_id: sessionId,
5696
6663
  });
5697
6664
  const result = await dispatchCodexNativeHook({
5698
6665
  hook_event_name: "Stop",
@@ -5700,18 +6667,257 @@ exit 0
5700
6667
  session_id: sessionId,
5701
6668
  }, { cwd });
5702
6669
  assert.equal(result.omxEventName, "stop");
5703
- assert.equal(result.outputJson, null);
6670
+ assert.equal(result.outputJson?.decision, "block");
6671
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
5704
6672
  }
5705
6673
  finally {
5706
6674
  await rm(cwd, { recursive: true, force: true });
5707
6675
  }
5708
6676
  });
5709
- it("returns an explicit ralplan waiting status while subagents are still active", async () => {
5710
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
6677
+ it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
6678
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
5711
6679
  try {
5712
6680
  const stateDir = join(cwd, ".omx", "state");
5713
- await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent"), { recursive: true });
5714
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent" });
6681
+ const sessionId = "sess-stop-unscoped-root-current-active";
6682
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6683
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6684
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6685
+ active: false,
6686
+ skill: "ralplan",
6687
+ phase: "complete",
6688
+ active_skills: [],
6689
+ });
6690
+ await writeJson(join(stateDir, "ralplan-state.json"), {
6691
+ active: false,
6692
+ mode: "ralplan",
6693
+ current_phase: "complete",
6694
+ });
6695
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6696
+ active: true,
6697
+ skill: "ralplan",
6698
+ phase: "planning",
6699
+ session_id: sessionId,
6700
+ active_skills: [{
6701
+ skill: "ralplan",
6702
+ phase: "planning",
6703
+ active: true,
6704
+ session_id: sessionId,
6705
+ }],
6706
+ });
6707
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6708
+ active: true,
6709
+ mode: "ralplan",
6710
+ current_phase: "planning",
6711
+ session_id: sessionId,
6712
+ });
6713
+ const result = await dispatchCodexNativeHook({
6714
+ hook_event_name: "Stop",
6715
+ cwd,
6716
+ session_id: sessionId,
6717
+ }, { cwd });
6718
+ assert.equal(result.omxEventName, "stop");
6719
+ assert.equal(result.outputJson?.decision, "block");
6720
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
6721
+ }
6722
+ finally {
6723
+ await rm(cwd, { recursive: true, force: true });
6724
+ }
6725
+ });
6726
+ it("does not block stale session ralplan when root ralplan is terminal and another root skill is active", async () => {
6727
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-ralplan-other-root-skill-"));
6728
+ try {
6729
+ const stateDir = join(cwd, ".omx", "state");
6730
+ const sessionId = "sess-stop-stale-ralplan-other-root-skill";
6731
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6732
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6733
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6734
+ active: true,
6735
+ skill: "deep-interview",
6736
+ phase: "intent-first",
6737
+ session_id: sessionId,
6738
+ active_skills: [{
6739
+ skill: "deep-interview",
6740
+ phase: "intent-first",
6741
+ active: true,
6742
+ session_id: sessionId,
6743
+ }],
6744
+ });
6745
+ await writeJson(join(stateDir, "ralplan-state.json"), {
6746
+ active: false,
6747
+ mode: "ralplan",
6748
+ current_phase: "complete",
6749
+ session_id: sessionId,
6750
+ });
6751
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6752
+ active: true,
6753
+ skill: "ralplan",
6754
+ phase: "planning",
6755
+ session_id: sessionId,
6756
+ active_skills: [{
6757
+ skill: "ralplan",
6758
+ phase: "planning",
6759
+ active: true,
6760
+ session_id: sessionId,
6761
+ }],
6762
+ });
6763
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6764
+ active: true,
6765
+ mode: "ralplan",
6766
+ current_phase: "planning",
6767
+ session_id: sessionId,
6768
+ });
6769
+ const result = await dispatchCodexNativeHook({
6770
+ hook_event_name: "Stop",
6771
+ cwd,
6772
+ session_id: sessionId,
6773
+ }, { cwd });
6774
+ assert.equal(result.omxEventName, "stop");
6775
+ assert.equal(result.outputJson, null);
6776
+ }
6777
+ finally {
6778
+ await rm(cwd, { recursive: true, force: true });
6779
+ }
6780
+ });
6781
+ it("keeps blocking session ralplan when canonical root state is not inactive", async () => {
6782
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-active-"));
6783
+ try {
6784
+ const stateDir = join(cwd, ".omx", "state");
6785
+ const sessionId = "sess-stop-session-ralplan-root-active";
6786
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6787
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6788
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6789
+ active: true,
6790
+ skill: "ralplan",
6791
+ phase: "planning",
6792
+ session_id: sessionId,
6793
+ active_skills: [{
6794
+ skill: "ralplan",
6795
+ phase: "planning",
6796
+ active: true,
6797
+ session_id: sessionId,
6798
+ }],
6799
+ });
6800
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6801
+ active: true,
6802
+ skill: "ralplan",
6803
+ phase: "planning",
6804
+ session_id: sessionId,
6805
+ active_skills: [{
6806
+ skill: "ralplan",
6807
+ phase: "planning",
6808
+ active: true,
6809
+ session_id: sessionId,
6810
+ }],
6811
+ });
6812
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6813
+ active: true,
6814
+ mode: "ralplan",
6815
+ current_phase: "planning",
6816
+ session_id: sessionId,
6817
+ });
6818
+ const result = await dispatchCodexNativeHook({
6819
+ hook_event_name: "Stop",
6820
+ cwd,
6821
+ session_id: sessionId,
6822
+ }, { cwd });
6823
+ assert.equal(result.omxEventName, "stop");
6824
+ assert.equal(result.outputJson?.decision, "block");
6825
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
6826
+ }
6827
+ finally {
6828
+ await rm(cwd, { recursive: true, force: true });
6829
+ }
6830
+ });
6831
+ it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
6832
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
6833
+ try {
6834
+ const stateDir = join(cwd, ".omx", "state");
6835
+ const sessionId = "sess-stop-terminal-ralplan";
6836
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6837
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6838
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6839
+ active: true,
6840
+ skill: "ralplan",
6841
+ phase: "planning",
6842
+ session_id: sessionId,
6843
+ active_skills: [{
6844
+ skill: "ralplan",
6845
+ phase: "planning",
6846
+ active: true,
6847
+ session_id: sessionId,
6848
+ }],
6849
+ });
6850
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6851
+ active: true,
6852
+ mode: "ralplan",
6853
+ current_phase: "planning",
6854
+ session_id: sessionId,
6855
+ });
6856
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
6857
+ version: 1,
6858
+ mode: "ralplan",
6859
+ active: false,
6860
+ outcome: "finish",
6861
+ lifecycle_outcome: "finished",
6862
+ current_phase: "complete",
6863
+ completed_at: "2026-05-01T00:00:00.000Z",
6864
+ updated_at: "2026-05-01T00:00:00.000Z",
6865
+ });
6866
+ const result = await dispatchCodexNativeHook({
6867
+ hook_event_name: "Stop",
6868
+ cwd,
6869
+ session_id: sessionId,
6870
+ }, { cwd });
6871
+ assert.equal(result.omxEventName, "stop");
6872
+ assert.equal(result.outputJson, null);
6873
+ }
6874
+ finally {
6875
+ await rm(cwd, { recursive: true, force: true });
6876
+ }
6877
+ });
6878
+ it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
6879
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
6880
+ try {
6881
+ const stateDir = join(cwd, ".omx", "state");
6882
+ const sessionId = "sess-stop-current-ralplan";
6883
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6884
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6885
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6886
+ active: true,
6887
+ skill: "ralplan",
6888
+ phase: "planning",
6889
+ session_id: sessionId,
6890
+ active_skills: [{
6891
+ skill: "ralplan",
6892
+ phase: "planning",
6893
+ active: true,
6894
+ session_id: sessionId,
6895
+ }],
6896
+ });
6897
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6898
+ active: true,
6899
+ mode: "ralplan",
6900
+ current_phase: "planning",
6901
+ session_id: "sess-other-ralplan",
6902
+ });
6903
+ const result = await dispatchCodexNativeHook({
6904
+ hook_event_name: "Stop",
6905
+ cwd,
6906
+ session_id: sessionId,
6907
+ }, { cwd });
6908
+ assert.equal(result.omxEventName, "stop");
6909
+ assert.equal(result.outputJson, null);
6910
+ }
6911
+ finally {
6912
+ await rm(cwd, { recursive: true, force: true });
6913
+ }
6914
+ });
6915
+ it("returns an explicit ralplan waiting status while subagents are still active", async () => {
6916
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
6917
+ try {
6918
+ const stateDir = join(cwd, ".omx", "state");
6919
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent"), { recursive: true });
6920
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent" });
5715
6921
  await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "skill-active-state.json"), {
5716
6922
  active: true,
5717
6923
  skill: "ralplan",
@@ -6406,16 +7612,25 @@ exit 0
6406
7612
  assert.equal(result.omxEventName, "stop");
6407
7613
  const reason = String(result.outputJson?.reason);
6408
7614
  assert.match(reason, /Ralph completion audit is missing required evidence/);
6409
- assert.match(reason, /state\.completion_audit = \{ passed: true, prompt_to_artifact_checklist: \[\.\.\.\], verification_evidence: \[\.\.\.\] \}/);
7615
+ assert.match(reason, /set "completion_audit" on the Ralph state object/);
7616
+ assert.doesNotMatch(reason, /state\.completion_audit/);
6410
7617
  assert.match(reason, /repo-relative JSON file/);
6411
7618
  assert.match(reason, /Markdown artifacts and flat top-level checklist\/evidence fields are not accepted/);
6412
7619
  assert.equal(result.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
6413
7620
  const reopened = JSON.parse(await readFile(statePath, "utf-8"));
6414
- assert.equal(reopened.active, true);
6415
- assert.equal(reopened.current_phase, "verifying");
7621
+ assert.equal(reopened.active, false);
7622
+ assert.equal(reopened.current_phase, "complete");
6416
7623
  assert.equal(reopened.completion_audit_gate, "blocked");
6417
7624
  assert.equal(reopened.completion_audit_missing_reason, "missing_completion_audit");
6418
- assert.equal(typeof reopened.completed_at, "undefined");
7625
+ assert.equal(reopened.completed_at, "2026-05-10T12:00:00.000Z");
7626
+ const repeat = await dispatchCodexNativeHook({
7627
+ hook_event_name: "Stop",
7628
+ cwd,
7629
+ session_id: sessionId,
7630
+ last_assistant_message: "Done. Ralph complete.",
7631
+ }, { cwd });
7632
+ assert.equal(repeat.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
7633
+ assert.doesNotMatch(String(repeat.outputJson?.reason), /Ralph is still active/);
6419
7634
  }
6420
7635
  finally {
6421
7636
  await rm(cwd, { recursive: true, force: true });
@@ -6605,6 +7820,42 @@ exit 0
6605
7820
  await rm(cwd, { recursive: true, force: true });
6606
7821
  }
6607
7822
  });
7823
+ it("allows Stop from stale orphaned session-scoped Ralph starting iteration zero state", async () => {
7824
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-orphan-starting-ralph-"));
7825
+ try {
7826
+ const stateDir = join(cwd, ".omx", "state");
7827
+ const sessionId = "sess-stale-orphan-ralph";
7828
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
7829
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
7830
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
7831
+ active: true,
7832
+ mode: "ralph",
7833
+ current_phase: "starting",
7834
+ iteration: 0,
7835
+ session_id: sessionId,
7836
+ updated_at: "2000-01-01T00:00:00.000Z",
7837
+ });
7838
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
7839
+ active: true,
7840
+ skill: "ralph",
7841
+ phase: "starting",
7842
+ session_id: sessionId,
7843
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
7844
+ });
7845
+ const result = await dispatchCodexNativeHook({
7846
+ hook_event_name: "Stop",
7847
+ cwd,
7848
+ session_id: sessionId,
7849
+ thread_id: "thread-verifier-terminal",
7850
+ last_assistant_message: "APPROVE: read-only verifier evidence is fresh and sufficient.",
7851
+ }, { cwd });
7852
+ assert.equal(result.omxEventName, "stop");
7853
+ assert.equal(result.outputJson, null);
7854
+ }
7855
+ finally {
7856
+ await rm(cwd, { recursive: true, force: true });
7857
+ }
7858
+ });
6608
7859
  it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
6609
7860
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
6610
7861
  try {
@@ -6640,6 +7891,126 @@ exit 0
6640
7891
  await rm(cwd, { recursive: true, force: true });
6641
7892
  }
6642
7893
  });
7894
+ it("retires prompt-seeded Ralph starting state when canonical Ralph already completed with audit", async () => {
7895
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-starting-"));
7896
+ try {
7897
+ const stateDir = join(cwd, ".omx", "state");
7898
+ const nativeSessionId = "native-hook-seed";
7899
+ const canonicalSessionId = "omx-runtime-session";
7900
+ await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
7901
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
7902
+ await writeJson(join(stateDir, "session.json"), {
7903
+ session_id: canonicalSessionId,
7904
+ cwd,
7905
+ });
7906
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
7907
+ active: true,
7908
+ mode: "ralph",
7909
+ current_phase: "starting",
7910
+ session_id: nativeSessionId,
7911
+ iteration: 0,
7912
+ task_slug: "mvp-h-local-method-preflight-execution",
7913
+ started_at: "2026-05-14T07:00:00.000Z",
7914
+ });
7915
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
7916
+ active: true,
7917
+ skill: "ralph",
7918
+ phase: "starting",
7919
+ session_id: nativeSessionId,
7920
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
7921
+ });
7922
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
7923
+ active: false,
7924
+ mode: "ralph",
7925
+ current_phase: "complete",
7926
+ session_id: canonicalSessionId,
7927
+ completed_at: "2026-05-14T07:30:00.000Z",
7928
+ completion_audit: {
7929
+ passed: true,
7930
+ prompt_to_artifact_checklist: ["task evidence mapped"],
7931
+ verification_evidence: ["fresh verification evidence recorded"],
7932
+ },
7933
+ });
7934
+ const result = await dispatchCodexNativeHook({
7935
+ hook_event_name: "Stop",
7936
+ cwd,
7937
+ session_id: nativeSessionId,
7938
+ }, { cwd });
7939
+ assert.equal(result.omxEventName, "stop");
7940
+ assert.equal(result.outputJson, null);
7941
+ const retiredState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
7942
+ assert.equal(retiredState.active, false);
7943
+ assert.equal(retiredState.current_phase, "complete");
7944
+ assert.equal(retiredState.stop_reason, "shadowed_by_completed_canonical_ralph");
7945
+ assert.equal(retiredState.shadowed_by_completed_canonical_ralph.session_id, canonicalSessionId);
7946
+ }
7947
+ finally {
7948
+ await rm(cwd, { recursive: true, force: true });
7949
+ }
7950
+ });
7951
+ it("does not retire prompt-seeded Ralph starting state from a completed canonical Ralph owned by another thread", async () => {
7952
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-thread-mismatch-"));
7953
+ try {
7954
+ const stateDir = join(cwd, ".omx", "state");
7955
+ const nativeSessionId = "native-hook-seed";
7956
+ const canonicalSessionId = "omx-runtime-session";
7957
+ await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
7958
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
7959
+ await writeJson(join(stateDir, "session.json"), {
7960
+ session_id: canonicalSessionId,
7961
+ cwd,
7962
+ });
7963
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
7964
+ active: true,
7965
+ mode: "ralph",
7966
+ current_phase: "starting",
7967
+ session_id: nativeSessionId,
7968
+ iteration: 0,
7969
+ task_slug: "mvp-h-local-method-preflight-execution",
7970
+ started_at: "2026-05-14T07:00:00.000Z",
7971
+ });
7972
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
7973
+ active: true,
7974
+ skill: "ralph",
7975
+ phase: "starting",
7976
+ session_id: nativeSessionId,
7977
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
7978
+ });
7979
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
7980
+ active: false,
7981
+ mode: "ralph",
7982
+ current_phase: "complete",
7983
+ session_id: canonicalSessionId,
7984
+ owner_codex_thread_id: "thread-A",
7985
+ completed_at: "2026-05-14T07:30:00.000Z",
7986
+ completion_audit: {
7987
+ passed: true,
7988
+ prompt_to_artifact_checklist: ["task evidence mapped"],
7989
+ verification_evidence: ["fresh verification evidence recorded"],
7990
+ },
7991
+ });
7992
+ const result = await dispatchCodexNativeHook({
7993
+ hook_event_name: "Stop",
7994
+ cwd,
7995
+ session_id: nativeSessionId,
7996
+ thread_id: "thread-B",
7997
+ }, { cwd });
7998
+ assert.equal(result.omxEventName, "stop");
7999
+ assert.deepEqual(result.outputJson, {
8000
+ decision: "block",
8001
+ reason: "OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
8002
+ stopReason: "ralph_starting",
8003
+ systemMessage: "OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
8004
+ });
8005
+ const preservedState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
8006
+ assert.equal(preservedState.active, true);
8007
+ assert.equal(preservedState.current_phase, "starting");
8008
+ assert.equal(preservedState.stop_reason, undefined);
8009
+ }
8010
+ finally {
8011
+ await rm(cwd, { recursive: true, force: true });
8012
+ }
8013
+ });
6643
8014
  it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
6644
8015
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
6645
8016
  try {