oh-my-codex 0.18.13 → 0.18.15

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 (360) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +41 -18
  4. package/dist/agents/__tests__/native-config.test.js +62 -0
  5. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  6. package/dist/agents/native-config.d.ts.map +1 -1
  7. package/dist/agents/native-config.js +15 -2
  8. package/dist/agents/native-config.js.map +1 -1
  9. package/dist/autopilot/__tests__/fsm.test.js +7 -0
  10. package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
  11. package/dist/autopilot/__tests__/planner-routing.test.d.ts +2 -0
  12. package/dist/autopilot/__tests__/planner-routing.test.d.ts.map +1 -0
  13. package/dist/autopilot/__tests__/planner-routing.test.js +74 -0
  14. package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -0
  15. package/dist/autopilot/__tests__/ralplan-gate.test.js +315 -0
  16. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  17. package/dist/autopilot/completion-gate.js +1 -1
  18. package/dist/autopilot/completion-gate.js.map +1 -1
  19. package/dist/autopilot/fsm.d.ts +2 -2
  20. package/dist/autopilot/fsm.d.ts.map +1 -1
  21. package/dist/autopilot/fsm.js +3 -0
  22. package/dist/autopilot/fsm.js.map +1 -1
  23. package/dist/autopilot/planner-routing.d.ts +21 -0
  24. package/dist/autopilot/planner-routing.d.ts.map +1 -0
  25. package/dist/autopilot/planner-routing.js +59 -0
  26. package/dist/autopilot/planner-routing.js.map +1 -0
  27. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  28. package/dist/autopilot/ralplan-gate.js +30 -0
  29. package/dist/autopilot/ralplan-gate.js.map +1 -1
  30. package/dist/autoresearch/goal.d.ts.map +1 -1
  31. package/dist/autoresearch/goal.js +10 -1
  32. package/dist/autoresearch/goal.js.map +1 -1
  33. package/dist/cli/__tests__/autoresearch-goal.test.js +33 -0
  34. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  35. package/dist/cli/__tests__/autoresearch-guided.test.js +3 -0
  36. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  37. package/dist/cli/__tests__/codex-plugin-layout.test.js +30 -0
  38. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  39. package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts +2 -0
  40. package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts.map +1 -0
  41. package/dist/cli/__tests__/doctor-artifact-ownership.test.js +158 -0
  42. package/dist/cli/__tests__/doctor-artifact-ownership.test.js.map +1 -0
  43. package/dist/cli/__tests__/doctor-explore-deprecated.test.d.ts +2 -0
  44. package/dist/cli/__tests__/doctor-explore-deprecated.test.d.ts.map +1 -0
  45. package/dist/cli/__tests__/doctor-explore-deprecated.test.js +33 -0
  46. package/dist/cli/__tests__/doctor-explore-deprecated.test.js.map +1 -0
  47. package/dist/cli/__tests__/doctor-spark-routing.test.js +13 -0
  48. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
  49. package/dist/cli/__tests__/doctor-warning-copy.test.js +132 -2
  50. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  51. package/dist/cli/__tests__/index.test.js +5 -0
  52. package/dist/cli/__tests__/index.test.js.map +1 -1
  53. package/dist/cli/__tests__/mcp-parity.test.js +40 -0
  54. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  55. package/dist/cli/__tests__/nested-help-routing.test.js +4 -0
  56. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  57. package/dist/cli/__tests__/package-bin-contract.test.js +7 -1
  58. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  59. package/dist/cli/__tests__/performance-goal.test.js +22 -0
  60. package/dist/cli/__tests__/performance-goal.test.js.map +1 -1
  61. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.d.ts +2 -0
  62. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.d.ts.map +1 -0
  63. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +64 -0
  64. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -0
  65. package/dist/cli/__tests__/resume.test.js +96 -0
  66. package/dist/cli/__tests__/resume.test.js.map +1 -1
  67. package/dist/cli/__tests__/session-scoped-runtime.test.js +162 -0
  68. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  69. package/dist/cli/__tests__/session-search.test.js +36 -0
  70. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  71. package/dist/cli/__tests__/setup-agents-overwrite.test.js +70 -1
  72. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  73. package/dist/cli/__tests__/setup-install-mode.test.js +33 -0
  74. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  75. package/dist/cli/__tests__/team.test.js +17 -3
  76. package/dist/cli/__tests__/team.test.js.map +1 -1
  77. package/dist/cli/__tests__/ultragoal.test.js +37 -0
  78. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  79. package/dist/cli/__tests__/update.test.js +81 -2
  80. package/dist/cli/__tests__/update.test.js.map +1 -1
  81. package/dist/cli/__tests__/url.test.d.ts +2 -0
  82. package/dist/cli/__tests__/url.test.d.ts.map +1 -0
  83. package/dist/cli/__tests__/url.test.js +82 -0
  84. package/dist/cli/__tests__/url.test.js.map +1 -0
  85. package/dist/cli/autoresearch-goal.d.ts.map +1 -1
  86. package/dist/cli/autoresearch-goal.js +2 -1
  87. package/dist/cli/autoresearch-goal.js.map +1 -1
  88. package/dist/cli/autoresearch-intake.d.ts.map +1 -1
  89. package/dist/cli/autoresearch-intake.js +2 -0
  90. package/dist/cli/autoresearch-intake.js.map +1 -1
  91. package/dist/cli/doctor.d.ts +39 -1
  92. package/dist/cli/doctor.d.ts.map +1 -1
  93. package/dist/cli/doctor.js +460 -40
  94. package/dist/cli/doctor.js.map +1 -1
  95. package/dist/cli/index.d.ts +2 -2
  96. package/dist/cli/index.d.ts.map +1 -1
  97. package/dist/cli/index.js +71 -4
  98. package/dist/cli/index.js.map +1 -1
  99. package/dist/cli/performance-goal.d.ts.map +1 -1
  100. package/dist/cli/performance-goal.js +4 -2
  101. package/dist/cli/performance-goal.js.map +1 -1
  102. package/dist/cli/plugin-marketplace.d.ts +1 -0
  103. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  104. package/dist/cli/plugin-marketplace.js +88 -67
  105. package/dist/cli/plugin-marketplace.js.map +1 -1
  106. package/dist/cli/project-runtime-codex-homes.d.ts +2 -0
  107. package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -1
  108. package/dist/cli/project-runtime-codex-homes.js +112 -5
  109. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  110. package/dist/cli/setup.d.ts.map +1 -1
  111. package/dist/cli/setup.js +15 -6
  112. package/dist/cli/setup.js.map +1 -1
  113. package/dist/cli/ultragoal.d.ts.map +1 -1
  114. package/dist/cli/ultragoal.js +5 -1
  115. package/dist/cli/ultragoal.js.map +1 -1
  116. package/dist/cli/update.d.ts.map +1 -1
  117. package/dist/cli/update.js +23 -4
  118. package/dist/cli/update.js.map +1 -1
  119. package/dist/cli/url.d.ts +8 -0
  120. package/dist/cli/url.d.ts.map +1 -0
  121. package/dist/cli/url.js +69 -0
  122. package/dist/cli/url.js.map +1 -0
  123. package/dist/config/__tests__/codex-hooks.test.js +20 -4
  124. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  125. package/dist/config/__tests__/models.test.js +21 -1
  126. package/dist/config/__tests__/models.test.js.map +1 -1
  127. package/dist/config/codex-hooks.d.ts.map +1 -1
  128. package/dist/config/codex-hooks.js +1 -4
  129. package/dist/config/codex-hooks.js.map +1 -1
  130. package/dist/config/models.d.ts +5 -0
  131. package/dist/config/models.d.ts.map +1 -1
  132. package/dist/config/models.js +23 -0
  133. package/dist/config/models.js.map +1 -1
  134. package/dist/goal-workflows/codex-goal-snapshot.d.ts +2 -0
  135. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  136. package/dist/goal-workflows/codex-goal-snapshot.js +10 -0
  137. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  138. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +7 -0
  139. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  140. package/dist/hooks/__tests__/keyword-detector.test.js +46 -0
  141. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  142. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +100 -0
  143. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  144. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +75 -0
  145. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  146. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +225 -0
  147. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  148. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +75 -0
  149. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  150. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +563 -0
  151. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  152. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +187 -12
  153. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  154. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +225 -0
  155. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  156. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  157. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  158. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +75 -0
  159. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  160. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  161. package/dist/hooks/keyword-detector.js +4 -0
  162. package/dist/hooks/keyword-detector.js.map +1 -1
  163. package/dist/hud/__tests__/reconcile.test.js +137 -2
  164. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  165. package/dist/hud/__tests__/state.test.js +107 -0
  166. package/dist/hud/__tests__/state.test.js.map +1 -1
  167. package/dist/hud/__tests__/tmux.test.js +17 -0
  168. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  169. package/dist/hud/reconcile.d.ts.map +1 -1
  170. package/dist/hud/reconcile.js +21 -7
  171. package/dist/hud/reconcile.js.map +1 -1
  172. package/dist/hud/render.d.ts.map +1 -1
  173. package/dist/hud/render.js +9 -0
  174. package/dist/hud/render.js.map +1 -1
  175. package/dist/hud/state.d.ts.map +1 -1
  176. package/dist/hud/state.js +43 -5
  177. package/dist/hud/state.js.map +1 -1
  178. package/dist/hud/types.d.ts +6 -0
  179. package/dist/hud/types.d.ts.map +1 -1
  180. package/dist/hud/types.js.map +1 -1
  181. package/dist/index.d.ts +1 -0
  182. package/dist/index.d.ts.map +1 -1
  183. package/dist/index.js +1 -0
  184. package/dist/index.js.map +1 -1
  185. package/dist/mcp/__tests__/bootstrap.test.js +24 -1
  186. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  187. package/dist/mcp/__tests__/state-paths.test.js +53 -0
  188. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  189. package/dist/mcp/bootstrap.d.ts +2 -0
  190. package/dist/mcp/bootstrap.d.ts.map +1 -1
  191. package/dist/mcp/bootstrap.js +32 -6
  192. package/dist/mcp/bootstrap.js.map +1 -1
  193. package/dist/mcp/state-paths.d.ts.map +1 -1
  194. package/dist/mcp/state-paths.js +11 -5
  195. package/dist/mcp/state-paths.js.map +1 -1
  196. package/dist/modes/__tests__/base-autopilot-gates.test.js +48 -0
  197. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  198. package/dist/modes/base.d.ts.map +1 -1
  199. package/dist/modes/base.js +1 -0
  200. package/dist/modes/base.js.map +1 -1
  201. package/dist/notifications/__tests__/http-client.test.js +9 -0
  202. package/dist/notifications/__tests__/http-client.test.js.map +1 -1
  203. package/dist/notifications/http-client.js +1 -1
  204. package/dist/notifications/http-client.js.map +1 -1
  205. package/dist/performance-goal/artifacts.d.ts.map +1 -1
  206. package/dist/performance-goal/artifacts.js +10 -1
  207. package/dist/performance-goal/artifacts.js.map +1 -1
  208. package/dist/ralplan/__tests__/consensus-gate.test.js +112 -0
  209. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  210. package/dist/ralplan/consensus-gate.d.ts +23 -0
  211. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  212. package/dist/ralplan/consensus-gate.js +133 -21
  213. package/dist/ralplan/consensus-gate.js.map +1 -1
  214. package/dist/scripts/__tests__/codex-native-hook.test.js +1251 -13
  215. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  216. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  217. package/dist/scripts/codex-native-hook.js +601 -24
  218. package/dist/scripts/codex-native-hook.js.map +1 -1
  219. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +1 -0
  220. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  221. package/dist/scripts/notify-hook/team-leader-nudge.js +279 -86
  222. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  223. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  224. package/dist/scripts/notify-hook/team-tmux-guard.js +87 -8
  225. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  226. package/dist/scripts/notify-hook/tmux-injection.js +1 -1
  227. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  228. package/dist/scripts/run-compiled-ci.d.ts +2 -0
  229. package/dist/scripts/run-compiled-ci.d.ts.map +1 -0
  230. package/dist/scripts/run-compiled-ci.js +77 -0
  231. package/dist/scripts/run-compiled-ci.js.map +1 -0
  232. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  233. package/dist/scripts/sync-plugin-mirror.js +1 -0
  234. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  235. package/dist/session-history/search.d.ts.map +1 -1
  236. package/dist/session-history/search.js +24 -9
  237. package/dist/session-history/search.js.map +1 -1
  238. package/dist/sidecar/__tests__/args.test.d.ts +2 -0
  239. package/dist/sidecar/__tests__/args.test.d.ts.map +1 -0
  240. package/dist/sidecar/__tests__/args.test.js +18 -0
  241. package/dist/sidecar/__tests__/args.test.js.map +1 -0
  242. package/dist/sidecar/index.d.ts.map +1 -1
  243. package/dist/sidecar/index.js +12 -4
  244. package/dist/sidecar/index.js.map +1 -1
  245. package/dist/state/__tests__/operations.test.js +107 -3
  246. package/dist/state/__tests__/operations.test.js.map +1 -1
  247. package/dist/state/operations.d.ts.map +1 -1
  248. package/dist/state/operations.js +57 -2
  249. package/dist/state/operations.js.map +1 -1
  250. package/dist/team/__tests__/delivery-e2e-smoke.test.js +22 -1
  251. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  252. package/dist/team/__tests__/model-contract.test.js +57 -1
  253. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  254. package/dist/team/__tests__/runtime.test.js +1468 -110
  255. package/dist/team/__tests__/runtime.test.js.map +1 -1
  256. package/dist/team/__tests__/scaling.test.js +63 -0
  257. package/dist/team/__tests__/scaling.test.js.map +1 -1
  258. package/dist/team/__tests__/state.test.js +24 -1
  259. package/dist/team/__tests__/state.test.js.map +1 -1
  260. package/dist/team/__tests__/tmux-session.test.js +512 -0
  261. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  262. package/dist/team/__tests__/worker-bootstrap.test.js +42 -9
  263. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  264. package/dist/team/model-contract.d.ts +16 -0
  265. package/dist/team/model-contract.d.ts.map +1 -1
  266. package/dist/team/model-contract.js +41 -1
  267. package/dist/team/model-contract.js.map +1 -1
  268. package/dist/team/runtime.d.ts.map +1 -1
  269. package/dist/team/runtime.js +116 -30
  270. package/dist/team/runtime.js.map +1 -1
  271. package/dist/team/scaling.d.ts.map +1 -1
  272. package/dist/team/scaling.js +9 -1
  273. package/dist/team/scaling.js.map +1 -1
  274. package/dist/team/state/types.d.ts +2 -0
  275. package/dist/team/state/types.d.ts.map +1 -1
  276. package/dist/team/state/types.js.map +1 -1
  277. package/dist/team/state.d.ts +3 -0
  278. package/dist/team/state.d.ts.map +1 -1
  279. package/dist/team/state.js +20 -0
  280. package/dist/team/state.js.map +1 -1
  281. package/dist/team/tmux-session.d.ts +38 -3
  282. package/dist/team/tmux-session.d.ts.map +1 -1
  283. package/dist/team/tmux-session.js +275 -36
  284. package/dist/team/tmux-session.js.map +1 -1
  285. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  286. package/dist/team/worker-bootstrap.js +26 -1
  287. package/dist/team/worker-bootstrap.js.map +1 -1
  288. package/dist/ultragoal/__tests__/artifacts.test.js +388 -0
  289. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  290. package/dist/ultragoal/__tests__/docs-contract.test.js +3 -0
  291. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  292. package/dist/ultragoal/artifacts.d.ts +23 -0
  293. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  294. package/dist/ultragoal/artifacts.js +275 -12
  295. package/dist/ultragoal/artifacts.js.map +1 -1
  296. package/dist/url-reader/__tests__/url-reader.test.d.ts +2 -0
  297. package/dist/url-reader/__tests__/url-reader.test.d.ts.map +1 -0
  298. package/dist/url-reader/__tests__/url-reader.test.js +433 -0
  299. package/dist/url-reader/__tests__/url-reader.test.js.map +1 -0
  300. package/dist/url-reader/index.d.ts +4 -0
  301. package/dist/url-reader/index.d.ts.map +1 -0
  302. package/dist/url-reader/index.js +522 -0
  303. package/dist/url-reader/index.js.map +1 -0
  304. package/dist/url-reader/types.d.ts +41 -0
  305. package/dist/url-reader/types.d.ts.map +1 -0
  306. package/dist/url-reader/types.js +2 -0
  307. package/dist/url-reader/types.js.map +1 -0
  308. package/dist/utils/__tests__/agents-md.test.js +39 -1
  309. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  310. package/dist/utils/__tests__/agents-model-table.test.js +28 -0
  311. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  312. package/dist/utils/agents-md.d.ts +4 -0
  313. package/dist/utils/agents-md.d.ts.map +1 -1
  314. package/dist/utils/agents-md.js +28 -0
  315. package/dist/utils/agents-md.js.map +1 -1
  316. package/dist/utils/agents-model-table.d.ts +7 -3
  317. package/dist/utils/agents-model-table.d.ts.map +1 -1
  318. package/dist/utils/agents-model-table.js +16 -8
  319. package/dist/utils/agents-model-table.js.map +1 -1
  320. package/dist/utils/platform-command.d.ts +7 -1
  321. package/dist/utils/platform-command.d.ts.map +1 -1
  322. package/dist/utils/platform-command.js +12 -1
  323. package/dist/utils/platform-command.js.map +1 -1
  324. package/dist/verification/__tests__/ci-rust-gates.test.js +4 -0
  325. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  326. package/dist/vscode/__tests__/index.test.d.ts +2 -0
  327. package/dist/vscode/__tests__/index.test.d.ts.map +1 -0
  328. package/dist/vscode/__tests__/index.test.js +236 -0
  329. package/dist/vscode/__tests__/index.test.js.map +1 -0
  330. package/dist/vscode/index.d.ts +76 -0
  331. package/dist/vscode/index.d.ts.map +1 -0
  332. package/dist/vscode/index.js +196 -0
  333. package/dist/vscode/index.js.map +1 -0
  334. package/package.json +3 -3
  335. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  336. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +59 -8
  337. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +9 -5
  338. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +3 -0
  339. package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +3 -0
  340. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  341. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +22 -4
  342. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +2 -2
  343. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +61 -0
  344. package/skills/autopilot/SKILL.md +9 -5
  345. package/skills/autoresearch-goal/SKILL.md +3 -0
  346. package/skills/ecomode/SKILL.md +1 -1
  347. package/skills/ecomode/references/agent-tiers.md +61 -0
  348. package/skills/performance-goal/SKILL.md +3 -0
  349. package/skills/ralplan/SKILL.md +1 -1
  350. package/skills/ultragoal/SKILL.md +22 -4
  351. package/skills/ultrawork/SKILL.md +2 -2
  352. package/skills/ultrawork/references/agent-tiers.md +61 -0
  353. package/src/scripts/__tests__/codex-native-hook.test.ts +1560 -181
  354. package/src/scripts/codex-native-hook.ts +663 -24
  355. package/src/scripts/notify-hook/team-leader-nudge.ts +298 -91
  356. package/src/scripts/notify-hook/team-tmux-guard.ts +92 -8
  357. package/src/scripts/notify-hook/tmux-injection.ts +1 -1
  358. package/src/scripts/run-compiled-ci.ts +85 -0
  359. package/src/scripts/sync-plugin-mirror.ts +1 -0
  360. package/templates/AGENTS.md +6 -0
@@ -214,6 +214,31 @@ if [[ "$cmd" == "capture-pane" ]]; then
214
214
  printf '%s\\n' ${captureText}
215
215
  exit 0
216
216
  fi
217
+ if [[ "$cmd" == "set-buffer" ]]; then
218
+ printf '%s' "\${@: -1}" > "${tmuxLogPath}.buffer"
219
+ exit 0
220
+ fi
221
+ if [[ "$cmd" == "show-buffer" ]]; then
222
+ if [[ -f "${tmuxLogPath}.buffer" ]]; then cat "${tmuxLogPath}.buffer"; fi
223
+ exit 0
224
+ fi
225
+ if [[ "$cmd" == "paste-buffer" ]]; then
226
+ target=""
227
+ while [[ "$#" -gt 0 ]]; do
228
+ case "$1" in
229
+ -t) target="$2"; shift 2 ;;
230
+ *) shift ;;
231
+ esac
232
+ done
233
+ if [[ -f "${tmuxLogPath}.buffer" ]]; then
234
+ echo "send-keys -t \${target} -l $(cat "${tmuxLogPath}.buffer")" >> "${tmuxLogPath}"
235
+ fi
236
+ exit 0
237
+ fi
238
+ if [[ "$cmd" == "delete-buffer" ]]; then
239
+ rm -f "${tmuxLogPath}.buffer"
240
+ exit 0
241
+ fi
217
242
  if [[ "$cmd" == "send-keys" ]]; then
218
243
  ${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
219
244
  ${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
@@ -501,9 +526,9 @@ describe("codex native hook dispatch", () => {
501
526
  hookSpecificOutput?: unknown;
502
527
  };
503
528
 
504
- assert.equal(output.continue, false);
529
+ assert.equal(output.continue, undefined);
505
530
  assert.equal(output.decision, undefined);
506
- assert.equal(output.stopReason, "native_hook_stdin_parse_error");
531
+ assert.equal(output.stopReason, undefined);
507
532
  assert.equal(output.hookSpecificOutput, undefined);
508
533
  assert.match(
509
534
  String(output.systemMessage ?? ""),
@@ -598,6 +623,159 @@ describe("codex native hook dispatch", () => {
598
623
  );
599
624
  });
600
625
 
626
+ it("emits no-op JSON stdout for PreToolUse non-Bash tools with null output", async () => {
627
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-nonbash-noop-"));
628
+ try {
629
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
630
+ cwd,
631
+ input: JSON.stringify({
632
+ hook_event_name: "PreToolUse",
633
+ cwd,
634
+ session_id: "sess-cli-pretool-nonbash-noop",
635
+ thread_id: "thread-cli-pretool-nonbash-noop",
636
+ turn_id: "turn-cli-pretool-nonbash-noop",
637
+ tool_name: "Read",
638
+ tool_input: { file_path: "package.json" },
639
+ }),
640
+ encoding: "utf-8",
641
+ stdio: ["pipe", "pipe", "pipe"],
642
+ });
643
+
644
+ assert.equal(result.status, 0, result.stderr || result.stdout);
645
+ assert.equal(result.stderr, "");
646
+ assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
647
+ } finally {
648
+ await rm(cwd, { recursive: true, force: true });
649
+ }
650
+ });
651
+
652
+ it("emits PreToolUse CLI advisory JSON with only systemMessage", async () => {
653
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-schema-safe-"));
654
+ try {
655
+ const output = parseSingleJsonStdout(runNativeHookCli({
656
+ hook_event_name: "PreToolUse",
657
+ cwd,
658
+ session_id: "sess-cli-pretool-schema-safe",
659
+ thread_id: "thread-cli-pretool-schema-safe",
660
+ turn_id: "turn-cli-pretool-schema-safe",
661
+ tool_name: "Bash",
662
+ tool_input: { command: "rm -rf dist" },
663
+ }, { cwd }));
664
+
665
+ assert.deepEqual(output, {
666
+ systemMessage:
667
+ "Destructive Bash command detected (`rm -rf dist`). Confirm the target and expected side effects before running it.",
668
+ });
669
+ } finally {
670
+ await rm(cwd, { recursive: true, force: true });
671
+ }
672
+ });
673
+
674
+ it("emits PreToolUse CLI block JSON with only systemMessage", async () => {
675
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-block-schema-safe-"));
676
+ try {
677
+ const output = parseSingleJsonStdout(runNativeHookCli({
678
+ hook_event_name: "PreToolUse",
679
+ cwd,
680
+ session_id: "sess-cli-pretool-block-schema-safe",
681
+ thread_id: "thread-cli-pretool-block-schema-safe",
682
+ turn_id: "turn-cli-pretool-block-schema-safe",
683
+ tool_name: "Bash",
684
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
685
+ }, { cwd }));
686
+
687
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
688
+ assert.match(String(output.systemMessage ?? ""), /Lore protocol/);
689
+ assert.equal(output.decision, undefined);
690
+ assert.equal(output.stopReason, undefined);
691
+ assert.equal(output.hookSpecificOutput, undefined);
692
+ } finally {
693
+ await rm(cwd, { recursive: true, force: true });
694
+ }
695
+ });
696
+
697
+ it("preserves ralplan PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
698
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-pretool-boundary-"));
699
+ const sessionId = "sess-cli-ralplan-pretool-boundary";
700
+ const stateDir = join(cwd, ".omx", "state");
701
+ try {
702
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
703
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
704
+ active: true,
705
+ skill: "ralplan",
706
+ phase: "planning",
707
+ session_id: sessionId,
708
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
709
+ });
710
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
711
+ active: true,
712
+ mode: "ralplan",
713
+ current_phase: "critic-review",
714
+ session_id: sessionId,
715
+ });
716
+
717
+ const output = parseSingleJsonStdout(runNativeHookCli({
718
+ hook_event_name: "PreToolUse",
719
+ cwd,
720
+ session_id: sessionId,
721
+ thread_id: "thread-cli-ralplan-pretool-boundary",
722
+ tool_name: "Edit",
723
+ tool_input: { file_path: "src/runtime.ts", old_string: "a", new_string: "b" },
724
+ }, { cwd }));
725
+
726
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
727
+ assert.match(String(output.systemMessage ?? ""), /Ralplan is active \(phase: critic-review\)/);
728
+ assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
729
+ assert.match(String(output.systemMessage ?? ""), /Write only planning artifacts/);
730
+ assert.equal(output.decision, undefined);
731
+ assert.equal(output.reason, undefined);
732
+ assert.equal(output.hookSpecificOutput, undefined);
733
+ } finally {
734
+ await rm(cwd, { recursive: true, force: true });
735
+ }
736
+ });
737
+
738
+ it("preserves deep-interview PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
739
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-deep-interview-pretool-boundary-"));
740
+ const sessionId = "sess-cli-deep-interview-pretool-boundary";
741
+ const stateDir = join(cwd, ".omx", "state");
742
+ try {
743
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
744
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
745
+ active: true,
746
+ skill: "deep-interview",
747
+ phase: "planning",
748
+ session_id: sessionId,
749
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId }],
750
+ });
751
+ await writeJson(join(stateDir, "sessions", sessionId, "deep-interview-state.json"), {
752
+ active: true,
753
+ mode: "deep-interview",
754
+ current_phase: "intent-first",
755
+ session_id: sessionId,
756
+ });
757
+
758
+ const output = parseSingleJsonStdout(runNativeHookCli({
759
+ hook_event_name: "PreToolUse",
760
+ cwd,
761
+ session_id: sessionId,
762
+ thread_id: "thread-cli-deep-interview-pretool-boundary",
763
+ tool_name: "Write",
764
+ tool_input: { file_path: "src/runtime.ts", content: "export const changed = true;\n" },
765
+ }, { cwd }));
766
+
767
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
768
+ assert.match(String(output.systemMessage ?? ""), /Deep-interview is active \(phase: intent-first\)/);
769
+ assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
770
+ assert.match(String(output.systemMessage ?? ""), /requirements\/spec mode/);
771
+ assert.equal(output.decision, undefined);
772
+ assert.equal(output.reason, undefined);
773
+ assert.equal(output.hookSpecificOutput, undefined);
774
+ } finally {
775
+ await rm(cwd, { recursive: true, force: true });
776
+ }
777
+ });
778
+
601
779
  it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
602
780
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
603
781
  try {
@@ -617,6 +795,32 @@ describe("codex native hook dispatch", () => {
617
795
  }
618
796
  });
619
797
 
798
+ it("emits no-op JSON stdout for Stop payloads with no runtime output", async () => {
799
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-null-output-"));
800
+ try {
801
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
802
+ cwd,
803
+ input: JSON.stringify({
804
+ hook_event_name: "Stop",
805
+ cwd,
806
+ session_id: "sess-cli-stop-null-output",
807
+ thread_id: "thread-cli-stop-null-output",
808
+ turn_id: "turn-cli-stop-null-output",
809
+ stop_hook_active: true,
810
+ }),
811
+ encoding: "utf-8",
812
+ stdio: ["pipe", "pipe", "pipe"],
813
+ });
814
+
815
+ assert.equal(result.status, 0, result.stderr || result.stdout);
816
+ assert.equal(result.stderr, "");
817
+ assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
818
+ assert.equal(existsSync(join(cwd, ".omx", "state")), false);
819
+ } finally {
820
+ await rm(cwd, { recursive: true, force: true });
821
+ }
822
+ });
823
+
620
824
  it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
621
825
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
622
826
  try {
@@ -760,6 +964,83 @@ describe("codex native hook dispatch", () => {
760
964
  }
761
965
  });
762
966
 
967
+ it("does not block Stop on stale session autopilot mirror when canonical skill state is inactive", async () => {
968
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stale-autopilot-mirror-"));
969
+ try {
970
+ const sessionId = "sess-stale-autopilot-mirror";
971
+ await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
972
+ await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
973
+ version: 1,
974
+ active: false,
975
+ skill: "",
976
+ phase: "cancelled",
977
+ active_skills: [],
978
+ });
979
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
980
+ active: true,
981
+ mode: "autopilot",
982
+ current_phase: "ultragoal",
983
+ session_id: sessionId,
984
+ });
985
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "skill-active-state.json"), {
986
+ version: 1,
987
+ active: true,
988
+ skill: "autopilot",
989
+ phase: "ultragoal",
990
+ session_id: sessionId,
991
+ active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
992
+ });
993
+
994
+ const stdout = runNativeHookCli({
995
+ hook_event_name: "Stop",
996
+ cwd,
997
+ session_id: sessionId,
998
+ thread_id: "thread-stale-autopilot-mirror",
999
+ }, { cwd });
1000
+
1001
+ assert.deepEqual(parseSingleJsonStdout(stdout), {});
1002
+ } finally {
1003
+ await rm(cwd, { recursive: true, force: true });
1004
+ }
1005
+ });
1006
+
1007
+ it("includes blocking state source and canonical agreement in Stop diagnostics", async () => {
1008
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-diagnostic-"));
1009
+ try {
1010
+ const sessionId = "sess-autopilot-diagnostic";
1011
+ await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
1012
+ await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
1013
+ version: 1,
1014
+ active: true,
1015
+ skill: "autopilot",
1016
+ phase: "ultragoal",
1017
+ session_id: sessionId,
1018
+ active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
1019
+ });
1020
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
1021
+ active: true,
1022
+ mode: "autopilot",
1023
+ current_phase: "ultragoal",
1024
+ session_id: sessionId,
1025
+ });
1026
+
1027
+ const output = parseSingleJsonStdout(runNativeHookCli({
1028
+ hook_event_name: "Stop",
1029
+ cwd,
1030
+ session_id: sessionId,
1031
+ thread_id: "thread-autopilot-diagnostic",
1032
+ }, { cwd })) as { decision?: string; reason?: string; statePath?: string; canonicalDisagreement?: string };
1033
+
1034
+ assert.equal(output.decision, "block");
1035
+ assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
1036
+ assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
1037
+ assert.equal(output.statePath, ".omx/state/sessions/sess-autopilot-diagnostic/autopilot-state.json");
1038
+ assert.equal(output.canonicalDisagreement, "canonical_agrees");
1039
+ } finally {
1040
+ await rm(cwd, { recursive: true, force: true });
1041
+ }
1042
+ });
1043
+
763
1044
  it("emits exactly one parseable JSON object for active Stop CLI continuation", async () => {
764
1045
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-json-"));
765
1046
  try {
@@ -2973,128 +3254,272 @@ standardMaxRounds = 15
2973
3254
  }
2974
3255
  });
2975
3256
 
2976
- it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
2977
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
3257
+ it("nudges next-goal prompts when a completed Codex goal cleanup step remains", async () => {
3258
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-prompt-"));
2978
3259
  try {
2979
- await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3260
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
2980
3261
  version: 1,
2981
- activeGoalId: "G001-demo",
2982
- goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
3262
+ workflow: "performance-goal",
3263
+ slug: "latency",
3264
+ objective: "Reduce latency",
3265
+ status: "complete",
3266
+ completedAt: "2026-05-20T00:00:00.000Z",
2983
3267
  });
2984
3268
 
2985
3269
  const result = await dispatchCodexNativeHook({
2986
- hook_event_name: "Stop",
3270
+ hook_event_name: "UserPromptSubmit",
2987
3271
  cwd,
2988
- session_id: "sess-ultragoal-generic-complete-stop",
2989
- thread_id: "thread-ultragoal-generic-complete-stop",
2990
- last_assistant_message: "Goal complete.",
3272
+ session_id: "sess-completed-goal-prompt",
3273
+ thread_id: "thread-completed-goal-prompt",
3274
+ prompt: "Start the next performance goal now",
2991
3275
  }, { cwd });
2992
3276
 
2993
- assert.equal(result.outputJson?.decision, "block");
2994
- assert.match(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3277
+ const output = JSON.stringify(result.outputJson);
3278
+ assert.match(output, /run \/goal clear/);
3279
+ assert.match(output, /before calling create_goal/);
3280
+ assert.match(output, /hooks only nudge and must not mutate Codex goal state/);
3281
+ assert.doesNotMatch(output, /cleared Codex goal state/i);
2995
3282
  } finally {
2996
3283
  await rm(cwd, { recursive: true, force: true });
2997
3284
  }
2998
3285
  });
2999
3286
 
3000
- it("does not block ultragoal Stop for ordinary prose about a goal to complete work", async () => {
3001
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-ordinary-stop-"));
3287
+ it("does not block ordinary Stop after completed Ultragoal cleanup remains", async () => {
3288
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-ultragoal-ordinary-stop-"));
3002
3289
  try {
3003
3290
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3004
3291
  version: 1,
3005
- activeGoalId: "G001-demo",
3006
- goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
3292
+ aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
3293
+ goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
3007
3294
  });
3008
3295
 
3009
3296
  const result = await dispatchCodexNativeHook({
3010
3297
  hook_event_name: "Stop",
3011
3298
  cwd,
3012
- session_id: "sess-ultragoal-ordinary-stop",
3013
- thread_id: "thread-ultragoal-ordinary-stop",
3014
- last_assistant_message: "My goal is to complete the migration without regressions, so I will keep testing.",
3299
+ session_id: "sess-completed-ultragoal-ordinary-stop",
3300
+ thread_id: "thread-completed-ultragoal-ordinary-stop",
3301
+ last_assistant_message: "Implemented the requested fix and verified the focused tests.",
3015
3302
  }, { cwd });
3016
3303
 
3017
- assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
3018
- assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3304
+ const output = JSON.stringify(result.outputJson);
3305
+ assert.notEqual(result.outputJson?.decision, "block");
3306
+ assert.notEqual(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
3307
+ assert.doesNotMatch(output, /run \/goal clear/);
3019
3308
  } finally {
3020
3309
  await rm(cwd, { recursive: true, force: true });
3021
3310
  }
3022
3311
  });
3023
3312
 
3024
- it("blocks ultragoal Stop with blocked checkpoint and available-goal-context remediation for completed legacy snapshots", async () => {
3025
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
3313
+ it("does not treat explanatory create_goal warnings as completed-goal cleanup attempts", async () => {
3314
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-explainer-stop-"));
3026
3315
  try {
3027
3316
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3028
3317
  version: 1,
3029
- activeGoalId: "G001-demo",
3030
- goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
3318
+ aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
3319
+ goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
3320
+ });
3321
+
3322
+ for (const [index, last_assistant_message] of [
3323
+ "Do not call create_goal until the user has explicitly cleared the old goal state.",
3324
+ "No create_goal attempt was made; this is only the final summary.",
3325
+ "I am not calling create_goal; the completed work is summarized above.",
3326
+ "I am not starting another goal; cleanup is already documented.",
3327
+ "Do not start another goal until cleanup is explicit.",
3328
+ "Do not create a new ultragoal; this is only a final summary.",
3329
+ ].entries()) {
3330
+ const result = await dispatchCodexNativeHook({
3331
+ hook_event_name: "Stop",
3332
+ cwd,
3333
+ session_id: `sess-completed-goal-explainer-stop-${index}`,
3334
+ thread_id: `thread-completed-goal-explainer-stop-${index}`,
3335
+ last_assistant_message,
3336
+ }, { cwd });
3337
+
3338
+ const output = JSON.stringify(result.outputJson);
3339
+ assert.notEqual(result.outputJson?.decision, "block");
3340
+ assert.notEqual(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
3341
+ assert.doesNotMatch(output, /run \/goal clear/);
3342
+ }
3343
+ } finally {
3344
+ await rm(cwd, { recursive: true, force: true });
3345
+ }
3346
+ });
3347
+
3348
+ it("blocks explicit create_goal attempts when fresh native-goal cleanup evidence remains", async () => {
3349
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-mixed-stop-"));
3350
+ try {
3351
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3352
+ version: 1,
3353
+ aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
3354
+ goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
3031
3355
  });
3032
3356
 
3033
3357
  const result = await dispatchCodexNativeHook({
3034
3358
  hook_event_name: "Stop",
3035
3359
  cwd,
3036
- session_id: "sess-ultragoal-legacy-stop",
3037
- thread_id: "thread-ultragoal-legacy-stop",
3038
- last_assistant_message: "get_goal returned a completed legacy goal, so ultragoal complete failed; marking complete now.",
3360
+ session_id: "sess-completed-goal-mixed-stop",
3361
+ thread_id: "thread-completed-goal-mixed-stop",
3362
+ last_user_message: "get_goal reports a completed Codex goal still attached to this thread; do not call create_goal until cleanup is explicit.",
3363
+ last_assistant_message: "I am starting another run now; create_goal payload follows.",
3039
3364
  }, { cwd });
3040
3365
 
3041
3366
  const output = JSON.stringify(result.outputJson);
3042
3367
  assert.equal(result.outputJson?.decision, "block");
3043
- assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3044
- assert.match(output, /--status blocked/);
3045
- assert.match(output, /Codex goal context/);
3046
- assert.match(output, /no such table: thread_goals/);
3047
- assert.match(output, /unavailable get_goal error JSON or path/);
3048
- assert.match(output, /safe-recovery blocker/);
3049
- assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
3050
- assert.match(output, /Hooks must not mutate Codex goal state/);
3368
+ assert.equal(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
3369
+ assert.match(output, /run \/goal clear/);
3051
3370
  } finally {
3052
3371
  await rm(cwd, { recursive: true, force: true });
3053
3372
  }
3054
3373
  });
3055
3374
 
3056
- it("does not repeat ultragoal Stop recovery after a safe completed-aggregate microgoal blocker is recorded", async () => {
3057
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-aggregate-blocked-stop-"));
3375
+ it("does not block Stop when completed durable history alone precedes another goal", async () => {
3376
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-stop-"));
3058
3377
  try {
3059
3378
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3060
3379
  version: 1,
3061
- codexGoalMode: "aggregate",
3062
- activeGoalId: "G001-demo",
3063
- goals: [{
3064
- id: "G001-demo",
3065
- status: "in_progress",
3066
- objective: "Demo goal",
3067
- failureReason: "aggregate Codex goal already complete and unreconcilable while repo-native .omx/ultragoal/goals.json still has an in-progress microgoal; stop the recovery loop",
3068
- }],
3380
+ aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
3381
+ goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
3069
3382
  });
3070
3383
 
3071
3384
  const result = await dispatchCodexNativeHook({
3072
3385
  hook_event_name: "Stop",
3073
3386
  cwd,
3074
- session_id: "sess-ultragoal-aggregate-blocked-stop",
3075
- thread_id: "thread-ultragoal-aggregate-blocked-stop",
3076
- stop_hook_active: true,
3077
- last_assistant_message: "Goal complete.",
3387
+ session_id: "sess-completed-goal-stop",
3388
+ thread_id: "thread-completed-goal-stop",
3389
+ last_assistant_message: "Starting another ultragoal now; create_goal payload follows after /goal clear already cleared native state.",
3078
3390
  }, { cwd });
3079
3391
 
3392
+ const output = JSON.stringify(result.outputJson);
3080
3393
  assert.notEqual(result.outputJson?.decision, "block");
3081
- assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
3082
- assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3394
+ assert.notEqual(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
3395
+ assert.doesNotMatch(output, /run \/goal clear/);
3083
3396
  } finally {
3084
3397
  await rm(cwd, { recursive: true, force: true });
3085
3398
  }
3086
3399
  });
3087
3400
 
3088
-
3089
- it("does not block ultragoal Stop after task-scoped reconciliation finishes exploded bookkeeping", async () => {
3090
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
3401
+ it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
3402
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
3091
3403
  try {
3092
3404
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3093
3405
  version: 1,
3094
- codexGoalMode: "aggregate",
3095
- 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.",
3096
- activeGoalId: "G001-micro",
3097
- aggregateCompletion: {
3406
+ activeGoalId: "G001-demo",
3407
+ goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
3408
+ });
3409
+
3410
+ const result = await dispatchCodexNativeHook({
3411
+ hook_event_name: "Stop",
3412
+ cwd,
3413
+ session_id: "sess-ultragoal-generic-complete-stop",
3414
+ thread_id: "thread-ultragoal-generic-complete-stop",
3415
+ last_assistant_message: "Goal complete.",
3416
+ }, { cwd });
3417
+
3418
+ assert.equal(result.outputJson?.decision, "block");
3419
+ assert.match(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3420
+ } finally {
3421
+ await rm(cwd, { recursive: true, force: true });
3422
+ }
3423
+ });
3424
+
3425
+ it("does not block ultragoal Stop for ordinary prose about a goal to complete work", async () => {
3426
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-ordinary-stop-"));
3427
+ try {
3428
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3429
+ version: 1,
3430
+ activeGoalId: "G001-demo",
3431
+ goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
3432
+ });
3433
+
3434
+ const result = await dispatchCodexNativeHook({
3435
+ hook_event_name: "Stop",
3436
+ cwd,
3437
+ session_id: "sess-ultragoal-ordinary-stop",
3438
+ thread_id: "thread-ultragoal-ordinary-stop",
3439
+ last_assistant_message: "My goal is to complete the migration without regressions, so I will keep testing.",
3440
+ }, { cwd });
3441
+
3442
+ assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
3443
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3444
+ } finally {
3445
+ await rm(cwd, { recursive: true, force: true });
3446
+ }
3447
+ });
3448
+
3449
+ it("blocks ultragoal Stop with blocked checkpoint and available-goal-context remediation for completed legacy snapshots", async () => {
3450
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
3451
+ try {
3452
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3453
+ version: 1,
3454
+ activeGoalId: "G001-demo",
3455
+ goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
3456
+ });
3457
+
3458
+ const result = await dispatchCodexNativeHook({
3459
+ hook_event_name: "Stop",
3460
+ cwd,
3461
+ session_id: "sess-ultragoal-legacy-stop",
3462
+ thread_id: "thread-ultragoal-legacy-stop",
3463
+ last_assistant_message: "get_goal returned a completed legacy goal, so ultragoal complete failed; marking complete now.",
3464
+ }, { cwd });
3465
+
3466
+ const output = JSON.stringify(result.outputJson);
3467
+ assert.equal(result.outputJson?.decision, "block");
3468
+ assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3469
+ assert.match(output, /--status blocked/);
3470
+ assert.match(output, /Codex goal context/);
3471
+ assert.match(output, /no such table: thread_goals/);
3472
+ assert.match(output, /unavailable get_goal error JSON or path/);
3473
+ assert.match(output, /safe-recovery blocker/);
3474
+ assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
3475
+ assert.match(output, /Hooks must not mutate Codex goal state/);
3476
+ } finally {
3477
+ await rm(cwd, { recursive: true, force: true });
3478
+ }
3479
+ });
3480
+
3481
+ it("does not repeat ultragoal Stop recovery after a safe completed-aggregate microgoal blocker is recorded", async () => {
3482
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-aggregate-blocked-stop-"));
3483
+ try {
3484
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3485
+ version: 1,
3486
+ codexGoalMode: "aggregate",
3487
+ activeGoalId: "G001-demo",
3488
+ goals: [{
3489
+ id: "G001-demo",
3490
+ status: "in_progress",
3491
+ objective: "Demo goal",
3492
+ failureReason: "aggregate Codex goal already complete and unreconcilable while repo-native .omx/ultragoal/goals.json still has an in-progress microgoal; stop the recovery loop",
3493
+ }],
3494
+ });
3495
+
3496
+ const result = await dispatchCodexNativeHook({
3497
+ hook_event_name: "Stop",
3498
+ cwd,
3499
+ session_id: "sess-ultragoal-aggregate-blocked-stop",
3500
+ thread_id: "thread-ultragoal-aggregate-blocked-stop",
3501
+ stop_hook_active: true,
3502
+ last_assistant_message: "Goal complete.",
3503
+ }, { cwd });
3504
+
3505
+ assert.notEqual(result.outputJson?.decision, "block");
3506
+ assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
3507
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
3508
+ } finally {
3509
+ await rm(cwd, { recursive: true, force: true });
3510
+ }
3511
+ });
3512
+
3513
+
3514
+ it("does not block ultragoal Stop after task-scoped reconciliation finishes exploded bookkeeping", async () => {
3515
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
3516
+ try {
3517
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3518
+ version: 1,
3519
+ codexGoalMode: "aggregate",
3520
+ 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.",
3521
+ activeGoalId: "G001-micro",
3522
+ aggregateCompletion: {
3098
3523
  status: "complete",
3099
3524
  completedAt: "2026-05-04T10:04:00.000Z",
3100
3525
  evidence: "planned work done; validation complete; reviews clean",
@@ -6381,6 +6806,144 @@ exit 0
6381
6806
  }
6382
6807
  });
6383
6808
 
6809
+ it("allows Ralplan read-only inspection and planning artifact writes while blocking implementation targets", async () => {
6810
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-guard-"));
6811
+ try {
6812
+ const stateDir = join(cwd, ".omx", "state");
6813
+ const sessionId = "sess-ralplan-guard";
6814
+ const sessionDir = join(stateDir, "sessions", sessionId);
6815
+ await mkdir(sessionDir, { recursive: true });
6816
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
6817
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6818
+ version: 1,
6819
+ active: true,
6820
+ skill: "ralplan",
6821
+ phase: "planning",
6822
+ session_id: sessionId,
6823
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
6824
+ });
6825
+ await writeJson(join(sessionDir, "ralplan-state.json"), {
6826
+ active: true,
6827
+ mode: "ralplan",
6828
+ current_phase: "planning",
6829
+ session_id: sessionId,
6830
+ });
6831
+
6832
+ const preToolUse = async (tool_name: string, tool_use_id: string, tool_input: Record<string, unknown>) => dispatchCodexNativeHook(
6833
+ {
6834
+ hook_event_name: "PreToolUse",
6835
+ cwd,
6836
+ session_id: sessionId,
6837
+ tool_name,
6838
+ tool_use_id,
6839
+ tool_input,
6840
+ },
6841
+ { cwd },
6842
+ );
6843
+
6844
+ const readOnlyCommands = [
6845
+ "git status --short",
6846
+ "ls -1 .omx/plans",
6847
+ "find .omx/plans -type f -name '*.md'",
6848
+ "grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
6849
+ "rg \"Scholastic\" doc tests .omx/plans .omx/context",
6850
+ "sed -n '1,80p' .omx/plans/demo.md",
6851
+ "cat .omx/plans/demo.md",
6852
+ "git status --short; ls -1 .omx/plans; grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
6853
+ ];
6854
+ for (const [index, command] of readOnlyCommands.entries()) {
6855
+ const result = await preToolUse("Bash", `tool-ralplan-read-${index}`, { command });
6856
+ assert.equal(result.outputJson, null, `read-only command should be allowed: ${command}`);
6857
+ }
6858
+
6859
+ for (const path of [
6860
+ ".omx/context/findings.md",
6861
+ ".omx/plans/issue-2863.md",
6862
+ ".omx/specs/issue-2863.md",
6863
+ ".omx/state/required-planning-state.json",
6864
+ ]) {
6865
+ const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
6866
+ assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
6867
+ }
6868
+
6869
+ const allowedPatchAdd = await preToolUse("apply_patch", "tool-ralplan-patch-add", {
6870
+ input: "*** Begin Patch\n*** Add File: .omx/plans/issue-2863.md\n+# Plan\n*** End Patch\n",
6871
+ });
6872
+ assert.equal(allowedPatchAdd.outputJson, null);
6873
+
6874
+ const allowedPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-patch-update", {
6875
+ input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
6876
+ });
6877
+ assert.equal(allowedPatchUpdate.outputJson, null);
6878
+
6879
+ const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
6880
+ command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
6881
+ });
6882
+ assert.equal(allowedRedirect.outputJson, null);
6883
+
6884
+ const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
6885
+ command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
6886
+ });
6887
+ assert.equal(allowedTee.outputJson, null);
6888
+
6889
+ const blockedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-block", {
6890
+ command: "printf 'bad' > src/implementation.ts",
6891
+ });
6892
+ assert.equal((blockedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6893
+ const redirectReason = String((blockedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
6894
+ assert.match(redirectReason, /Bash redirect write/);
6895
+ assert.match(redirectReason, /src\/implementation\.ts/);
6896
+
6897
+ const blockedEdit = await preToolUse("Edit", "tool-ralplan-edit-block", {
6898
+ file_path: "src/implementation.ts",
6899
+ old_string: "a",
6900
+ new_string: "b",
6901
+ });
6902
+ assert.equal((blockedEdit.outputJson as { decision?: string } | null)?.decision, "block");
6903
+ const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
6904
+ assert.match(editReason, /Edit path/);
6905
+ assert.match(editReason, /src\/implementation\.ts/);
6906
+
6907
+ const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
6908
+ input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
6909
+ });
6910
+ assert.equal((blockedMixedPatch.outputJson as { decision?: string } | null)?.decision, "block");
6911
+ const mixedReason = String((blockedMixedPatch.outputJson as { reason?: string } | null)?.reason ?? "");
6912
+ assert.match(mixedReason, /apply_patch target/);
6913
+ assert.match(mixedReason, /src\/leak\.ts/);
6914
+
6915
+ const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
6916
+ input: "not a recognizable patch",
6917
+ });
6918
+ assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
6919
+ assert.match(String((blockedUnparseablePatch.outputJson as { reason?: string } | null)?.reason ?? ""), /apply_patch target extraction failed/);
6920
+
6921
+ const blockedUnresolvedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-unresolved", {
6922
+ command: "cat > \"$PLAN_PATH\" <<'EOF'\ncontent\nEOF",
6923
+ });
6924
+ assert.equal((blockedUnresolvedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6925
+ const unresolvedReason = String((blockedUnresolvedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
6926
+ assert.match(unresolvedReason, /unresolved Bash write target/);
6927
+ assert.match(unresolvedReason, /\$PLAN_PATH/);
6928
+
6929
+ const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
6930
+ file_path: ".omx/plans/../../src/leak.ts",
6931
+ content: "bad",
6932
+ });
6933
+ assert.equal((blockedTraversal.outputJson as { decision?: string } | null)?.decision, "block");
6934
+ assert.match(String((blockedTraversal.outputJson as { reason?: string } | null)?.reason ?? ""), /\.omx\/plans\/\.\.\/\.\.\/src\/leak\.ts/);
6935
+
6936
+ const blockedAbsolute = await preToolUse("Write", "tool-ralplan-absolute-block", {
6937
+ file_path: "/tmp/leak.ts",
6938
+ content: "bad",
6939
+ });
6940
+ assert.equal((blockedAbsolute.outputJson as { decision?: string } | null)?.decision, "block");
6941
+ assert.match(String((blockedAbsolute.outputJson as { reason?: string } | null)?.reason ?? ""), /\/tmp\/leak\.ts/);
6942
+ } finally {
6943
+ await rm(cwd, { recursive: true, force: true });
6944
+ }
6945
+ });
6946
+
6384
6947
  it("allows Autopilot ralplan planning artifacts while blocking implementation writes", async () => {
6385
6948
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-ralplan-artifact-"));
6386
6949
  try {
@@ -6461,87 +7024,344 @@ exit 0
6461
7024
  }
6462
7025
  });
6463
7026
 
6464
- it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
6465
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
7027
+ it("allows Ralplan Beads tracker metadata writes during planning", async () => {
7028
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-beads-"));
6466
7029
  try {
6467
7030
  const stateDir = join(cwd, ".omx", "state");
6468
- const sessionDir = join(stateDir, "sessions", "sess-di-null-redirect");
7031
+ const sessionDir = join(stateDir, "sessions", "sess-ralplan-beads");
6469
7032
  await mkdir(sessionDir, { recursive: true });
6470
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-null-redirect", cwd });
7033
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-ralplan-beads", cwd });
6471
7034
  await writeJson(join(sessionDir, "skill-active-state.json"), {
6472
7035
  version: 1,
6473
7036
  active: true,
6474
- skill: "deep-interview",
7037
+ skill: "ralplan",
6475
7038
  phase: "planning",
6476
- session_id: "sess-di-null-redirect",
6477
- active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-null-redirect" }],
7039
+ session_id: "sess-ralplan-beads",
7040
+ thread_id: "thread-ralplan-beads",
7041
+ active_skills: [
7042
+ {
7043
+ skill: "ralplan",
7044
+ phase: "planning",
7045
+ active: true,
7046
+ session_id: "sess-ralplan-beads",
7047
+ thread_id: "thread-ralplan-beads",
7048
+ },
7049
+ ],
6478
7050
  });
6479
- await writeJson(join(sessionDir, "deep-interview-state.json"), {
7051
+ await writeJson(join(sessionDir, "ralplan-state.json"), {
6480
7052
  active: true,
6481
- mode: "deep-interview",
6482
- current_phase: "intent-first",
6483
- session_id: "sess-di-null-redirect",
7053
+ mode: "ralplan",
7054
+ current_phase: "planning",
7055
+ session_id: "sess-ralplan-beads",
7056
+ thread_id: "thread-ralplan-beads",
6484
7057
  });
6485
7058
 
6486
- const allowedCommands = [
6487
- "find application -type d -name 'bug-tracking*' 2>/dev/null | head -20",
6488
- "find application -type d -name 'bug-tracking*' 2> /dev/null | head -20",
6489
- "find application -type d -name 'bug-tracking*' 2>NUL | head -20",
6490
- "find application -type d -name 'bug-tracking*' 1>/dev/null",
6491
- "find application -type d -name 'bug-tracking*' &>/dev/null",
6492
- ];
7059
+ const allowedWrite = await dispatchCodexNativeHook(
7060
+ {
7061
+ hook_event_name: "PreToolUse",
7062
+ cwd,
7063
+ session_id: "sess-ralplan-beads",
7064
+ thread_id: "thread-ralplan-beads",
7065
+ tool_name: "Write",
7066
+ tool_use_id: "tool-ralplan-beads-write",
7067
+ tool_input: { file_path: ".beads/issues.db", content: "metadata" },
7068
+ },
7069
+ { cwd },
7070
+ );
7071
+ assert.equal(allowedWrite.outputJson, null);
6493
7072
 
6494
- for (const [index, command] of allowedCommands.entries()) {
6495
- const result = await dispatchCodexNativeHook(
6496
- {
6497
- hook_event_name: "PreToolUse",
6498
- cwd,
6499
- session_id: "sess-di-null-redirect",
6500
- tool_name: "Bash",
6501
- tool_use_id: `tool-di-null-redirect-${index}`,
6502
- tool_input: { command },
7073
+ const allowedEdit = await dispatchCodexNativeHook(
7074
+ {
7075
+ hook_event_name: "PreToolUse",
7076
+ cwd,
7077
+ session_id: "sess-ralplan-beads",
7078
+ thread_id: "thread-ralplan-beads",
7079
+ tool_name: "Edit",
7080
+ tool_use_id: "tool-ralplan-beads-edit",
7081
+ tool_input: { file_path: ".beads/tasks/issue.json", old_string: "old", new_string: "new" },
7082
+ },
7083
+ { cwd },
7084
+ );
7085
+ assert.equal(allowedEdit.outputJson, null);
7086
+
7087
+ const allowedPatch = await dispatchCodexNativeHook(
7088
+ {
7089
+ hook_event_name: "PreToolUse",
7090
+ cwd,
7091
+ session_id: "sess-ralplan-beads",
7092
+ thread_id: "thread-ralplan-beads",
7093
+ tool_name: "apply_patch",
7094
+ tool_use_id: "tool-ralplan-beads-apply-patch",
7095
+ tool_input: {
7096
+ input: "*** Begin Patch\n*** Add File: .beads/tasks/issue.json\n+{}\n*** End Patch\n",
6503
7097
  },
6504
- { cwd },
6505
- );
6506
- assert.equal(result.outputJson, null, command);
6507
- }
7098
+ },
7099
+ { cwd },
7100
+ );
7101
+ assert.equal(allowedPatch.outputJson, null);
6508
7102
 
6509
- const blockedCommands = [
6510
- "find application -type d -name 'bug-tracking*' 2>errors.log | head -20",
6511
- "find application -type d -name 'bug-tracking*' > /tmp/bug-tracking.txt",
6512
- "find application -type d -name 'bug-tracking*' | tee /dev/null",
7103
+ const allowedCommands = [
7104
+ "bd --db .beads/issues.db create 'Issue title'",
7105
+ "bd --db .beads/issues.db update OMX-1 --title 'Issue title'",
7106
+ "bd --db .beads/issues.db edit OMX-1 --title 'Issue title'",
7107
+ "bd --db .beads/issues.db comments add OMX-1 'evidence'",
7108
+ "bd --db .beads/issues.db close OMX-1",
7109
+ "bd --db .beads/issues.db reopen OMX-1",
7110
+ "bd --db .beads/issues.db status OMX-1",
7111
+ "bd --db .beads/issues.db dep add OMX-1 OMX-2",
6513
7112
  ];
6514
7113
 
6515
- for (const [index, command] of blockedCommands.entries()) {
7114
+ for (const [index, command] of allowedCommands.entries()) {
6516
7115
  const result = await dispatchCodexNativeHook(
6517
7116
  {
6518
7117
  hook_event_name: "PreToolUse",
6519
7118
  cwd,
6520
- session_id: "sess-di-null-redirect",
7119
+ session_id: "sess-ralplan-beads",
7120
+ thread_id: "thread-ralplan-beads",
6521
7121
  tool_name: "Bash",
6522
- tool_use_id: `tool-di-real-redirect-${index}`,
7122
+ tool_use_id: `tool-ralplan-beads-bd-${index}`,
6523
7123
  tool_input: { command },
6524
7124
  },
6525
7125
  { cwd },
6526
7126
  );
6527
- assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
7127
+ assert.equal(result.outputJson, null, `expected Beads metadata command to be allowed: ${command}`);
6528
7128
  }
6529
7129
  } finally {
6530
7130
  await rm(cwd, { recursive: true, force: true });
6531
7131
  }
6532
7132
  });
6533
7133
 
6534
- it("allows implementation tools after an explicit deep-interview handoff deactivates the mode", async () => {
6535
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-handoff-"));
7134
+ it("blocks unsafe Beads tracker targets and mixed implementation writes during Autopilot ralplan planning", async () => {
7135
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-beads-block-"));
6536
7136
  try {
6537
7137
  const stateDir = join(cwd, ".omx", "state");
6538
- const sessionDir = join(stateDir, "sessions", "sess-di-handoff");
7138
+ const sessionDir = join(stateDir, "sessions", "sess-autopilot-beads-block");
6539
7139
  await mkdir(sessionDir, { recursive: true });
7140
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-beads-block", cwd });
6540
7141
  await writeJson(join(sessionDir, "skill-active-state.json"), {
6541
7142
  version: 1,
6542
7143
  active: true,
6543
- skill: "deep-interview",
6544
- phase: "planning",
7144
+ skill: "autopilot",
7145
+ phase: "ralplan",
7146
+ session_id: "sess-autopilot-beads-block",
7147
+ thread_id: "thread-autopilot-beads-block",
7148
+ active_skills: [
7149
+ {
7150
+ skill: "autopilot",
7151
+ phase: "ralplan",
7152
+ active: true,
7153
+ session_id: "sess-autopilot-beads-block",
7154
+ thread_id: "thread-autopilot-beads-block",
7155
+ },
7156
+ ],
7157
+ });
7158
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
7159
+ active: true,
7160
+ mode: "autopilot",
7161
+ current_phase: "ralplan",
7162
+ session_id: "sess-autopilot-beads-block",
7163
+ thread_id: "thread-autopilot-beads-block",
7164
+ });
7165
+
7166
+ const blockedCommands = [
7167
+ "bd --db /tmp/outside.db create 'Issue title'",
7168
+ "bd --db ../outside.db create 'Issue title'",
7169
+ "bd --db .beads create 'Issue title'",
7170
+ "bd --db $DB create 'Issue title'",
7171
+ "DB=.beads/issues.db bd --db $DB create 'Issue title'",
7172
+ "env bd --db /tmp/outside.db create 'Issue title'",
7173
+ "command bd --db /tmp/outside.db create 'Issue title'",
7174
+ "bd --db .beads/issues.db export",
7175
+ "bd --db .beads/issues.db create 'Issue title' > src/leak.ts",
7176
+ "bd --db .beads/issues.db create 'Issue title'; cat > src/leak.ts",
7177
+ ];
7178
+
7179
+ for (const [index, command] of blockedCommands.entries()) {
7180
+ const result = await dispatchCodexNativeHook(
7181
+ {
7182
+ hook_event_name: "PreToolUse",
7183
+ cwd,
7184
+ session_id: "sess-autopilot-beads-block",
7185
+ thread_id: "thread-autopilot-beads-block",
7186
+ tool_name: "Bash",
7187
+ tool_use_id: `tool-autopilot-beads-block-${index}`,
7188
+ tool_input: { command },
7189
+ },
7190
+ { cwd },
7191
+ );
7192
+ assert.equal(
7193
+ (result.outputJson as { decision?: string } | null)?.decision,
7194
+ "block",
7195
+ `expected unsafe Beads command to be blocked: ${command}`,
7196
+ );
7197
+ }
7198
+
7199
+ const mentionWithImplementationWrite = await dispatchCodexNativeHook(
7200
+ {
7201
+ hook_event_name: "PreToolUse",
7202
+ cwd,
7203
+ session_id: "sess-autopilot-beads-block",
7204
+ thread_id: "thread-autopilot-beads-block",
7205
+ tool_name: "Bash",
7206
+ tool_use_id: "tool-autopilot-beads-mention-write",
7207
+ tool_input: { command: "echo bd --db .beads/issues.db create > src/leak.ts" },
7208
+ },
7209
+ { cwd },
7210
+ );
7211
+ assert.equal((mentionWithImplementationWrite.outputJson as { decision?: string } | null)?.decision, "block");
7212
+ assert.match(String((mentionWithImplementationWrite.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
7213
+
7214
+ const blockedImplementationEdit = await dispatchCodexNativeHook(
7215
+ {
7216
+ hook_event_name: "PreToolUse",
7217
+ cwd,
7218
+ session_id: "sess-autopilot-beads-block",
7219
+ thread_id: "thread-autopilot-beads-block",
7220
+ tool_name: "Edit",
7221
+ tool_use_id: "tool-autopilot-beads-src-edit",
7222
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
7223
+ },
7224
+ { cwd },
7225
+ );
7226
+ assert.equal((blockedImplementationEdit.outputJson as { decision?: string } | null)?.decision, "block");
7227
+ } finally {
7228
+ await rm(cwd, { recursive: true, force: true });
7229
+ }
7230
+ });
7231
+
7232
+ it("allows Autopilot rework implementation writes while ralplan remains guarded", async () => {
7233
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-rework-"));
7234
+ try {
7235
+ const stateDir = join(cwd, ".omx", "state");
7236
+ const sessionDir = join(stateDir, "sessions", "sess-autopilot-rework");
7237
+ await mkdir(sessionDir, { recursive: true });
7238
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-rework", cwd });
7239
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
7240
+ version: 1,
7241
+ active: true,
7242
+ skill: "autopilot",
7243
+ phase: "rework",
7244
+ session_id: "sess-autopilot-rework",
7245
+ thread_id: "thread-autopilot-rework",
7246
+ active_skills: [
7247
+ {
7248
+ skill: "autopilot",
7249
+ phase: "rework",
7250
+ active: true,
7251
+ session_id: "sess-autopilot-rework",
7252
+ thread_id: "thread-autopilot-rework",
7253
+ },
7254
+ ],
7255
+ });
7256
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
7257
+ active: true,
7258
+ mode: "autopilot",
7259
+ current_phase: "rework",
7260
+ review_cycle: 2,
7261
+ review_verdict: { clean: false, recommendation: "REQUEST_CHANGES", findings: ["src/implementation.ts"] },
7262
+ session_id: "sess-autopilot-rework",
7263
+ thread_id: "thread-autopilot-rework",
7264
+ });
7265
+
7266
+ const allowedImplementationEdit = await dispatchCodexNativeHook(
7267
+ {
7268
+ hook_event_name: "PreToolUse",
7269
+ cwd,
7270
+ session_id: "sess-autopilot-rework",
7271
+ thread_id: "thread-autopilot-rework",
7272
+ tool_name: "Edit",
7273
+ tool_use_id: "tool-autopilot-rework-src-edit",
7274
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
7275
+ },
7276
+ { cwd },
7277
+ );
7278
+ assert.equal(allowedImplementationEdit.outputJson, null);
7279
+ } finally {
7280
+ await rm(cwd, { recursive: true, force: true });
7281
+ }
7282
+ });
7283
+
7284
+ it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
7285
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
7286
+ try {
7287
+ const stateDir = join(cwd, ".omx", "state");
7288
+ const sessionDir = join(stateDir, "sessions", "sess-di-null-redirect");
7289
+ await mkdir(sessionDir, { recursive: true });
7290
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-null-redirect", cwd });
7291
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
7292
+ version: 1,
7293
+ active: true,
7294
+ skill: "deep-interview",
7295
+ phase: "planning",
7296
+ session_id: "sess-di-null-redirect",
7297
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-null-redirect" }],
7298
+ });
7299
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
7300
+ active: true,
7301
+ mode: "deep-interview",
7302
+ current_phase: "intent-first",
7303
+ session_id: "sess-di-null-redirect",
7304
+ });
7305
+
7306
+ const allowedCommands = [
7307
+ "find application -type d -name 'bug-tracking*' 2>/dev/null | head -20",
7308
+ "find application -type d -name 'bug-tracking*' 2> /dev/null | head -20",
7309
+ "find application -type d -name 'bug-tracking*' 2>NUL | head -20",
7310
+ "find application -type d -name 'bug-tracking*' 1>/dev/null",
7311
+ "find application -type d -name 'bug-tracking*' &>/dev/null",
7312
+ ];
7313
+
7314
+ for (const [index, command] of allowedCommands.entries()) {
7315
+ const result = await dispatchCodexNativeHook(
7316
+ {
7317
+ hook_event_name: "PreToolUse",
7318
+ cwd,
7319
+ session_id: "sess-di-null-redirect",
7320
+ tool_name: "Bash",
7321
+ tool_use_id: `tool-di-null-redirect-${index}`,
7322
+ tool_input: { command },
7323
+ },
7324
+ { cwd },
7325
+ );
7326
+ assert.equal(result.outputJson, null, command);
7327
+ }
7328
+
7329
+ const blockedCommands = [
7330
+ "find application -type d -name 'bug-tracking*' 2>errors.log | head -20",
7331
+ "find application -type d -name 'bug-tracking*' > /tmp/bug-tracking.txt",
7332
+ "find application -type d -name 'bug-tracking*' | tee /dev/null",
7333
+ ];
7334
+
7335
+ for (const [index, command] of blockedCommands.entries()) {
7336
+ const result = await dispatchCodexNativeHook(
7337
+ {
7338
+ hook_event_name: "PreToolUse",
7339
+ cwd,
7340
+ session_id: "sess-di-null-redirect",
7341
+ tool_name: "Bash",
7342
+ tool_use_id: `tool-di-real-redirect-${index}`,
7343
+ tool_input: { command },
7344
+ },
7345
+ { cwd },
7346
+ );
7347
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
7348
+ }
7349
+ } finally {
7350
+ await rm(cwd, { recursive: true, force: true });
7351
+ }
7352
+ });
7353
+
7354
+ it("allows implementation tools after an explicit deep-interview handoff deactivates the mode", async () => {
7355
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-handoff-"));
7356
+ try {
7357
+ const stateDir = join(cwd, ".omx", "state");
7358
+ const sessionDir = join(stateDir, "sessions", "sess-di-handoff");
7359
+ await mkdir(sessionDir, { recursive: true });
7360
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
7361
+ version: 1,
7362
+ active: true,
7363
+ skill: "deep-interview",
7364
+ phase: "planning",
6545
7365
  session_id: "sess-di-handoff",
6546
7366
  active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-handoff" }],
6547
7367
  });
@@ -6630,6 +7450,207 @@ exit 0
6630
7450
  }
6631
7451
  });
6632
7452
 
7453
+ it("records native subagent capacity exhaustion from spawn_agent PostToolUse output", async () => {
7454
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-record-"));
7455
+ try {
7456
+ const result = await dispatchCodexNativeHook(
7457
+ {
7458
+ hook_event_name: "PostToolUse",
7459
+ cwd,
7460
+ session_id: "sess-subagent-capacity-record",
7461
+ thread_id: "thread-subagent-capacity-record",
7462
+ turn_id: "turn-subagent-capacity-record",
7463
+ tool_name: "multi_agent_v1.spawn_agent",
7464
+ tool_response: {
7465
+ error: "collab spawn failed: agent thread limit reached",
7466
+ },
7467
+ },
7468
+ { cwd },
7469
+ );
7470
+
7471
+ assert.equal(result.omxEventName, "post-tool-use");
7472
+ assert.equal(result.outputJson, null);
7473
+
7474
+ const blocker = JSON.parse(
7475
+ await readFile(join(cwd, ".omx", "state", "native-subagent-capacity-blocker.json"), "utf-8"),
7476
+ ) as Record<string, unknown>;
7477
+ assert.equal(blocker.reason, "agent_thread_limit_reached");
7478
+ assert.equal(blocker.session_id, "sess-subagent-capacity-record");
7479
+ assert.equal(blocker.thread_id, "thread-subagent-capacity-record");
7480
+ assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
7481
+ assert.match(String(blocker.error_summary), /agent thread limit reached/);
7482
+ assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
7483
+ } finally {
7484
+ await rm(cwd, { recursive: true, force: true });
7485
+ }
7486
+ });
7487
+
7488
+ it("blocks close_agent cleanup after recent native subagent capacity exhaustion", async () => {
7489
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-block-"));
7490
+ try {
7491
+ await dispatchCodexNativeHook(
7492
+ {
7493
+ hook_event_name: "PostToolUse",
7494
+ cwd,
7495
+ session_id: "sess-subagent-capacity-close-block",
7496
+ thread_id: "thread-subagent-capacity-close-block",
7497
+ turn_id: "turn-subagent-capacity-close-block",
7498
+ tool_name: "multi_agent_v1.spawn_agent",
7499
+ tool_response: "collab spawn failed: agent thread limit reached",
7500
+ },
7501
+ { cwd },
7502
+ );
7503
+
7504
+ const result = await dispatchCodexNativeHook(
7505
+ {
7506
+ hook_event_name: "PreToolUse",
7507
+ cwd,
7508
+ session_id: "sess-subagent-capacity-close-block",
7509
+ thread_id: "thread-subagent-capacity-close-block",
7510
+ tool_name: "multi_agent_v1.close_agent",
7511
+ tool_input: { target: "019ecc36-stale" },
7512
+ },
7513
+ { cwd },
7514
+ );
7515
+
7516
+ assert.equal(result.omxEventName, "pre-tool-use");
7517
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
7518
+ assert.match(JSON.stringify(result.outputJson), /agent thread limit reached/);
7519
+ assert.match(JSON.stringify(result.outputJson), /multi_agent_v1\.close_agent/);
7520
+ assert.match(JSON.stringify(result.outputJson), /multi_tool_use\.parallel/);
7521
+ assert.match(JSON.stringify(result.outputJson), /bounded capacity blocker/);
7522
+ } finally {
7523
+ await rm(cwd, { recursive: true, force: true });
7524
+ }
7525
+ });
7526
+
7527
+ it("emits schema-safe PreToolUse CLI stdout for close_agent capacity blocks", async () => {
7528
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-subagent-capacity-close-block-"));
7529
+ try {
7530
+ parseSingleJsonStdout(runNativeHookCli({
7531
+ hook_event_name: "PostToolUse",
7532
+ cwd,
7533
+ session_id: "sess-cli-subagent-capacity-close-block",
7534
+ thread_id: "thread-cli-subagent-capacity-close-block",
7535
+ turn_id: "turn-cli-subagent-capacity-close-block",
7536
+ tool_name: "multi_agent_v1.spawn_agent",
7537
+ tool_response: "collab spawn failed: agent thread limit reached",
7538
+ }, { cwd }));
7539
+
7540
+ const output = parseSingleJsonStdout(runNativeHookCli({
7541
+ hook_event_name: "PreToolUse",
7542
+ cwd,
7543
+ session_id: "sess-cli-subagent-capacity-close-block",
7544
+ thread_id: "thread-cli-subagent-capacity-close-block",
7545
+ tool_name: "multi_agent_v1.close_agent",
7546
+ tool_input: { target: "019ecc36-stale" },
7547
+ }, { cwd }));
7548
+
7549
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
7550
+ assert.match(String(output.systemMessage ?? ""), /agent thread limit reached/);
7551
+ assert.match(String(output.systemMessage ?? ""), /Do not call multi_agent_v1\.close_agent/);
7552
+ assert.equal(output.decision, undefined);
7553
+ assert.equal(output.hookSpecificOutput, undefined);
7554
+ } finally {
7555
+ await rm(cwd, { recursive: true, force: true });
7556
+ }
7557
+ });
7558
+
7559
+ it("blocks parallel close_agent cleanup after recent native subagent capacity exhaustion", async () => {
7560
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-parallel-close-block-"));
7561
+ try {
7562
+ await dispatchCodexNativeHook(
7563
+ {
7564
+ hook_event_name: "PostToolUse",
7565
+ cwd,
7566
+ session_id: "sess-subagent-capacity-parallel-close-block",
7567
+ thread_id: "thread-subagent-capacity-parallel-close-block",
7568
+ tool_name: "multi_agent_v1.spawn_agent",
7569
+ tool_response: "agent thread limit reached",
7570
+ },
7571
+ { cwd },
7572
+ );
7573
+
7574
+ const result = await dispatchCodexNativeHook(
7575
+ {
7576
+ hook_event_name: "PreToolUse",
7577
+ cwd,
7578
+ session_id: "sess-subagent-capacity-parallel-close-block",
7579
+ thread_id: "thread-subagent-capacity-parallel-close-block",
7580
+ tool_name: "multi_tool_use.parallel",
7581
+ tool_input: {
7582
+ tool_uses: [
7583
+ { recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-1" } },
7584
+ { recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-2" } },
7585
+ ],
7586
+ },
7587
+ },
7588
+ { cwd },
7589
+ );
7590
+
7591
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
7592
+ assert.match(JSON.stringify(result.outputJson), /Do not call multi_agent_v1\.close_agent/);
7593
+ assert.match(JSON.stringify(result.outputJson), /do not batch close_agent through multi_tool_use\.parallel/);
7594
+ } finally {
7595
+ await rm(cwd, { recursive: true, force: true });
7596
+ }
7597
+ });
7598
+
7599
+ it("does not block close_agent without a recent native capacity blocker", async () => {
7600
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-no-blocker-"));
7601
+ try {
7602
+ const result = await dispatchCodexNativeHook(
7603
+ {
7604
+ hook_event_name: "PreToolUse",
7605
+ cwd,
7606
+ session_id: "sess-subagent-capacity-close-no-blocker",
7607
+ thread_id: "thread-subagent-capacity-close-no-blocker",
7608
+ tool_name: "multi_agent_v1.close_agent",
7609
+ tool_input: { target: "completed-agent" },
7610
+ },
7611
+ { cwd },
7612
+ );
7613
+
7614
+ assert.equal(result.omxEventName, "pre-tool-use");
7615
+ assert.equal(result.outputJson, null);
7616
+ } finally {
7617
+ await rm(cwd, { recursive: true, force: true });
7618
+ }
7619
+ });
7620
+
7621
+ it("does not block close_agent when the native capacity blocker is expired", async () => {
7622
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-expired-"));
7623
+ try {
7624
+ const stateDir = join(cwd, ".omx", "state");
7625
+ await mkdir(stateDir, { recursive: true });
7626
+ await writeJson(join(stateDir, "native-subagent-capacity-blocker.json"), {
7627
+ schema_version: 1,
7628
+ reason: "agent_thread_limit_reached",
7629
+ session_id: "sess-subagent-capacity-close-expired",
7630
+ error_summary: "agent thread limit reached",
7631
+ observed_at: "2026-06-20T00:00:00.000Z",
7632
+ expires_at: "2026-06-20T00:30:00.000Z",
7633
+ cwd,
7634
+ });
7635
+
7636
+ const result = await dispatchCodexNativeHook(
7637
+ {
7638
+ hook_event_name: "PreToolUse",
7639
+ cwd,
7640
+ session_id: "sess-subagent-capacity-close-expired",
7641
+ thread_id: "thread-subagent-capacity-close-expired",
7642
+ tool_name: "multi_agent_v1.close_agent",
7643
+ tool_input: { target: "completed-agent" },
7644
+ },
7645
+ { cwd },
7646
+ );
7647
+
7648
+ assert.equal(result.outputJson, null);
7649
+ } finally {
7650
+ await rm(cwd, { recursive: true, force: true });
7651
+ }
7652
+ });
7653
+
6633
7654
  it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
6634
7655
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
6635
7656
  try {
@@ -11113,158 +12134,439 @@ exit 0
11113
12134
  {
11114
12135
  hook_event_name: "Stop",
11115
12136
  cwd,
11116
- session_id: "sess-stop-team-env-root",
12137
+ session_id: "sess-stop-team-env-root",
12138
+ },
12139
+ { cwd },
12140
+ );
12141
+
12142
+ assert.equal(result.omxEventName, "stop");
12143
+ assert.deepEqual(result.outputJson, {
12144
+ decision: "block",
12145
+ reason:
12146
+ `OMX team pipeline is still active (env-root-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
12147
+ stopReason: "team_team-exec",
12148
+ systemMessage: "OMX team pipeline is still active at phase team-exec.",
12149
+ });
12150
+ } finally {
12151
+ if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
12152
+ else delete process.env.OMX_TEAM_STATE_ROOT;
12153
+ await rm(cwd, { recursive: true, force: true });
12154
+ }
12155
+ });
12156
+
12157
+ it("blocks Stop from session-scoped team mode when session.json points to another session", async () => {
12158
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-mismatch-"));
12159
+ try {
12160
+ const stateDir = join(cwd, ".omx", "state");
12161
+ await mkdir(join(stateDir, "sessions", "sess-live-team"), { recursive: true });
12162
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-team" });
12163
+ await writeJson(join(stateDir, "sessions", "sess-live-team", "team-state.json"), {
12164
+ active: true,
12165
+ mode: "team",
12166
+ current_phase: "team-exec",
12167
+ team_name: "session-live-team",
12168
+ });
12169
+ await writeJson(join(stateDir, "team", "session-live-team", "phase.json"), {
12170
+ current_phase: "team-exec",
12171
+ max_fix_attempts: 3,
12172
+ current_fix_attempt: 0,
12173
+ transitions: [],
12174
+ updated_at: new Date().toISOString(),
12175
+ });
12176
+
12177
+ const result = await dispatchCodexNativeHook(
12178
+ {
12179
+ hook_event_name: "Stop",
12180
+ cwd,
12181
+ session_id: "sess-live-team",
12182
+ },
12183
+ { cwd },
12184
+ );
12185
+
12186
+ assert.equal(result.omxEventName, "stop");
12187
+ assert.deepEqual(result.outputJson, {
12188
+ decision: "block",
12189
+ reason:
12190
+ `OMX team pipeline is still active (session-live-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
12191
+ stopReason: "team_team-exec",
12192
+ systemMessage: "OMX team pipeline is still active at phase team-exec.",
12193
+ });
12194
+ } finally {
12195
+ await rm(cwd, { recursive: true, force: true });
12196
+ }
12197
+ });
12198
+
12199
+ it("returns Stop continuation output for active ralplan skill with matching active mode state and without active subagents", async () => {
12200
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-"));
12201
+ try {
12202
+ const stateDir = join(cwd, ".omx", "state");
12203
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill"), { recursive: true });
12204
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill" });
12205
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill", "skill-active-state.json"), {
12206
+ active: true,
12207
+ skill: "ralplan",
12208
+ phase: "planning",
12209
+ });
12210
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill", "ralplan-state.json"), {
12211
+ active: true,
12212
+ current_phase: "planning",
12213
+ });
12214
+
12215
+ const result = await dispatchCodexNativeHook(
12216
+ {
12217
+ hook_event_name: "Stop",
12218
+ cwd,
12219
+ session_id: "sess-stop-skill",
12220
+ },
12221
+ { cwd },
12222
+ );
12223
+
12224
+ assert.equal(result.omxEventName, "stop");
12225
+ assert.equal(result.outputJson?.decision, "block");
12226
+ assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
12227
+ assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
12228
+ assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
12229
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
12230
+ assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
12231
+ } finally {
12232
+ await rm(cwd, { recursive: true, force: true });
12233
+ }
12234
+ });
12235
+
12236
+ it("does not block Stop after owner-session ralplan CLI completion writes to native canonical state", async () => {
12237
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-owner-alias-complete-"));
12238
+ const previousSessionId = process.env.OMX_SESSION_ID;
12239
+ try {
12240
+ const stateDir = join(cwd, ".omx", "state");
12241
+ const nativeSessionId = "native-id";
12242
+ const ownerSessionId = "omx-owner-id";
12243
+ await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
12244
+ await writeJson(join(stateDir, "session.json"), {
12245
+ session_id: nativeSessionId,
12246
+ native_session_id: nativeSessionId,
12247
+ owner_omx_session_id: ownerSessionId,
12248
+ cwd,
12249
+ });
12250
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
12251
+ active: true,
12252
+ skill: "ralplan",
12253
+ phase: "planning",
12254
+ session_id: nativeSessionId,
12255
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: nativeSessionId }],
12256
+ });
12257
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "ralplan-state.json"), {
12258
+ active: true,
12259
+ mode: "ralplan",
12260
+ current_phase: "planning",
12261
+ session_id: nativeSessionId,
12262
+ });
12263
+ process.env.OMX_SESSION_ID = ownerSessionId;
12264
+
12265
+ const writeResult = await executeStateOperation("state_write", {
12266
+ mode: "ralplan",
12267
+ active: false,
12268
+ current_phase: "complete",
12269
+ status: "complete",
12270
+ terminal_state: "complete",
12271
+ workingDirectory: cwd,
12272
+ });
12273
+
12274
+ assert.notEqual(writeResult.isError, true);
12275
+ assert.equal(existsSync(join(stateDir, "sessions", ownerSessionId, "ralplan-state.json")), false);
12276
+ const result = await dispatchCodexNativeHook(
12277
+ {
12278
+ hook_event_name: "Stop",
12279
+ cwd,
12280
+ session_id: nativeSessionId,
12281
+ },
12282
+ { cwd },
12283
+ );
12284
+
12285
+ assert.equal(result.omxEventName, "stop");
12286
+ assert.equal(result.outputJson, null);
12287
+ } finally {
12288
+ if (typeof previousSessionId === "string") process.env.OMX_SESSION_ID = previousSessionId;
12289
+ else delete process.env.OMX_SESSION_ID;
12290
+ await rm(cwd, { recursive: true, force: true });
12291
+ }
12292
+ });
12293
+
12294
+ it("does not block on stale ralplan skill-active state when the matching mode state is absent", async () => {
12295
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-skill-"));
12296
+ try {
12297
+ const stateDir = join(cwd, ".omx", "state");
12298
+ await mkdir(join(stateDir, "sessions", "sess-stop-stale-skill"), { recursive: true });
12299
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-stale-skill" });
12300
+ await writeJson(join(stateDir, "sessions", "sess-stop-stale-skill", "skill-active-state.json"), {
12301
+ active: true,
12302
+ skill: "ralplan",
12303
+ phase: "planning",
12304
+ session_id: "sess-stop-stale-skill",
12305
+ active_skills: [{
12306
+ skill: "ralplan",
12307
+ phase: "planning",
12308
+ active: true,
12309
+ session_id: "sess-stop-stale-skill",
12310
+ }],
12311
+ });
12312
+
12313
+ const result = await dispatchCodexNativeHook(
12314
+ {
12315
+ hook_event_name: "Stop",
12316
+ cwd,
12317
+ session_id: "sess-stop-stale-skill",
12318
+ },
12319
+ { cwd },
12320
+ );
12321
+
12322
+ assert.equal(result.omxEventName, "stop");
12323
+ assert.equal(result.outputJson, null);
12324
+ } finally {
12325
+ await rm(cwd, { recursive: true, force: true });
12326
+ }
12327
+ });
12328
+
12329
+ it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
12330
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
12331
+ try {
12332
+ const stateDir = join(cwd, ".omx", "state");
12333
+ const sessionId = "sess-stop-stale-session-ralplan";
12334
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12335
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12336
+ await writeJson(join(stateDir, "skill-active-state.json"), {
12337
+ active: false,
12338
+ skill: "ralplan",
12339
+ phase: "reviewing",
12340
+ active_skills: [],
12341
+ });
12342
+ await writeJson(join(stateDir, "ralplan-state.json"), {
12343
+ active: false,
12344
+ mode: "ralplan",
12345
+ current_phase: "complete",
12346
+ session_id: sessionId,
12347
+ cwd,
12348
+ });
12349
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
12350
+ active: true,
12351
+ skill: "ralplan",
12352
+ phase: "planning",
12353
+ session_id: sessionId,
12354
+ active_skills: [{
12355
+ skill: "ralplan",
12356
+ phase: "planning",
12357
+ active: true,
12358
+ session_id: sessionId,
12359
+ }],
12360
+ });
12361
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12362
+ active: true,
12363
+ mode: "ralplan",
12364
+ current_phase: "planning",
12365
+ session_id: sessionId,
12366
+ });
12367
+
12368
+ const result = await dispatchCodexNativeHook(
12369
+ {
12370
+ hook_event_name: "Stop",
12371
+ cwd,
12372
+ session_id: sessionId,
11117
12373
  },
11118
12374
  { cwd },
11119
12375
  );
11120
12376
 
11121
12377
  assert.equal(result.omxEventName, "stop");
11122
- assert.deepEqual(result.outputJson, {
11123
- decision: "block",
11124
- reason:
11125
- `OMX team pipeline is still active (env-root-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
11126
- stopReason: "team_team-exec",
11127
- systemMessage: "OMX team pipeline is still active at phase team-exec.",
11128
- });
12378
+ assert.equal(result.outputJson, null);
11129
12379
  } finally {
11130
- if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
11131
- else delete process.env.OMX_TEAM_STATE_ROOT;
11132
12380
  await rm(cwd, { recursive: true, force: true });
11133
12381
  }
11134
12382
  });
11135
12383
 
11136
- it("blocks Stop from session-scoped team mode when session.json points to another session", async () => {
11137
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-mismatch-"));
12384
+ it("keeps blocking current session ralplan when canonical root inactive ralplan state lacks project context", async () => {
12385
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-canonical-root-no-cwd-"));
11138
12386
  try {
11139
12387
  const stateDir = join(cwd, ".omx", "state");
11140
- await mkdir(join(stateDir, "sessions", "sess-live-team"), { recursive: true });
11141
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-team" });
11142
- await writeJson(join(stateDir, "sessions", "sess-live-team", "team-state.json"), {
12388
+ const sessionId = "sess-stop-ralplan-canonical-root-no-cwd";
12389
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12390
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12391
+ await writeJson(join(stateDir, "skill-active-state.json"), {
12392
+ active: false,
12393
+ skill: "ralplan",
12394
+ phase: "complete",
12395
+ session_id: sessionId,
12396
+ active_skills: [],
12397
+ });
12398
+ await writeJson(join(stateDir, "ralplan-state.json"), {
12399
+ active: false,
12400
+ mode: "ralplan",
12401
+ current_phase: "complete",
12402
+ session_id: sessionId,
12403
+ });
12404
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11143
12405
  active: true,
11144
- mode: "team",
11145
- current_phase: "team-exec",
11146
- team_name: "session-live-team",
12406
+ skill: "ralplan",
12407
+ phase: "planning",
12408
+ session_id: sessionId,
12409
+ active_skills: [{
12410
+ skill: "ralplan",
12411
+ phase: "planning",
12412
+ active: true,
12413
+ session_id: sessionId,
12414
+ }],
11147
12415
  });
11148
- await writeJson(join(stateDir, "team", "session-live-team", "phase.json"), {
11149
- current_phase: "team-exec",
11150
- max_fix_attempts: 3,
11151
- current_fix_attempt: 0,
11152
- transitions: [],
11153
- updated_at: new Date().toISOString(),
12416
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12417
+ active: true,
12418
+ mode: "ralplan",
12419
+ current_phase: "planning",
12420
+ session_id: sessionId,
12421
+ cwd,
11154
12422
  });
11155
12423
 
11156
12424
  const result = await dispatchCodexNativeHook(
11157
12425
  {
11158
12426
  hook_event_name: "Stop",
11159
12427
  cwd,
11160
- session_id: "sess-live-team",
12428
+ session_id: sessionId,
11161
12429
  },
11162
12430
  { cwd },
11163
12431
  );
11164
12432
 
11165
12433
  assert.equal(result.omxEventName, "stop");
11166
- assert.deepEqual(result.outputJson, {
11167
- decision: "block",
11168
- reason:
11169
- `OMX team pipeline is still active (session-live-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
11170
- stopReason: "team_team-exec",
11171
- systemMessage: "OMX team pipeline is still active at phase team-exec.",
11172
- });
12434
+ assert.equal(result.outputJson?.decision, "block");
12435
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
11173
12436
  } finally {
11174
12437
  await rm(cwd, { recursive: true, force: true });
11175
12438
  }
11176
12439
  });
11177
12440
 
11178
- it("returns Stop continuation output for active ralplan skill with matching active mode state and without active subagents", async () => {
11179
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-"));
12441
+ it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
12442
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
11180
12443
  try {
11181
12444
  const stateDir = join(cwd, ".omx", "state");
11182
- await mkdir(join(stateDir, "sessions", "sess-stop-skill"), { recursive: true });
11183
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill" });
11184
- await writeJson(join(stateDir, "sessions", "sess-stop-skill", "skill-active-state.json"), {
12445
+ const sessionId = "sess-stop-current-active-ralplan";
12446
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12447
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12448
+ await writeJson(join(stateDir, "skill-active-state.json"), {
12449
+ active: false,
12450
+ skill: "ralplan",
12451
+ phase: "complete",
12452
+ session_id: "sess-stop-old-ralplan",
12453
+ active_skills: [],
12454
+ });
12455
+ await writeJson(join(stateDir, "ralplan-state.json"), {
12456
+ active: false,
12457
+ mode: "ralplan",
12458
+ current_phase: "complete",
12459
+ session_id: "sess-stop-old-ralplan",
12460
+ });
12461
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11185
12462
  active: true,
11186
12463
  skill: "ralplan",
11187
12464
  phase: "planning",
12465
+ session_id: sessionId,
12466
+ active_skills: [{
12467
+ skill: "ralplan",
12468
+ phase: "planning",
12469
+ active: true,
12470
+ session_id: sessionId,
12471
+ }],
11188
12472
  });
11189
- await writeJson(join(stateDir, "sessions", "sess-stop-skill", "ralplan-state.json"), {
12473
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11190
12474
  active: true,
12475
+ mode: "ralplan",
11191
12476
  current_phase: "planning",
12477
+ session_id: sessionId,
11192
12478
  });
11193
12479
 
11194
12480
  const result = await dispatchCodexNativeHook(
11195
12481
  {
11196
12482
  hook_event_name: "Stop",
11197
12483
  cwd,
11198
- session_id: "sess-stop-skill",
12484
+ session_id: sessionId,
11199
12485
  },
11200
12486
  { cwd },
11201
12487
  );
11202
12488
 
11203
12489
  assert.equal(result.omxEventName, "stop");
11204
12490
  assert.equal(result.outputJson?.decision, "block");
11205
- assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
11206
- assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
11207
- assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
11208
12491
  assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
11209
- assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
11210
12492
  } finally {
11211
12493
  await rm(cwd, { recursive: true, force: true });
11212
12494
  }
11213
12495
  });
11214
12496
 
11215
- it("does not block on stale ralplan skill-active state when the matching mode state is absent", async () => {
11216
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-skill-"));
12497
+ it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
12498
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
11217
12499
  try {
11218
12500
  const stateDir = join(cwd, ".omx", "state");
11219
- await mkdir(join(stateDir, "sessions", "sess-stop-stale-skill"), { recursive: true });
11220
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-stale-skill" });
11221
- await writeJson(join(stateDir, "sessions", "sess-stop-stale-skill", "skill-active-state.json"), {
12501
+ const sessionId = "sess-stop-unscoped-root-current-active";
12502
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12503
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12504
+ await writeJson(join(stateDir, "skill-active-state.json"), {
12505
+ active: false,
12506
+ skill: "ralplan",
12507
+ phase: "complete",
12508
+ active_skills: [],
12509
+ });
12510
+ await writeJson(join(stateDir, "ralplan-state.json"), {
12511
+ active: false,
12512
+ mode: "ralplan",
12513
+ current_phase: "complete",
12514
+ });
12515
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11222
12516
  active: true,
11223
12517
  skill: "ralplan",
11224
12518
  phase: "planning",
11225
- session_id: "sess-stop-stale-skill",
12519
+ session_id: sessionId,
11226
12520
  active_skills: [{
11227
12521
  skill: "ralplan",
11228
12522
  phase: "planning",
11229
12523
  active: true,
11230
- session_id: "sess-stop-stale-skill",
12524
+ session_id: sessionId,
11231
12525
  }],
11232
12526
  });
12527
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12528
+ active: true,
12529
+ mode: "ralplan",
12530
+ current_phase: "planning",
12531
+ session_id: sessionId,
12532
+ });
11233
12533
 
11234
12534
  const result = await dispatchCodexNativeHook(
11235
12535
  {
11236
12536
  hook_event_name: "Stop",
11237
12537
  cwd,
11238
- session_id: "sess-stop-stale-skill",
12538
+ session_id: sessionId,
11239
12539
  },
11240
12540
  { cwd },
11241
12541
  );
11242
12542
 
11243
12543
  assert.equal(result.omxEventName, "stop");
11244
- assert.equal(result.outputJson, null);
12544
+ assert.equal(result.outputJson?.decision, "block");
12545
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
11245
12546
  } finally {
11246
12547
  await rm(cwd, { recursive: true, force: true });
11247
12548
  }
11248
12549
  });
11249
12550
 
11250
- it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
11251
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
12551
+ it("keeps blocking current session ralplan when unscoped root completion lacks a plan artifact", async () => {
12552
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-no-plan-artifact-"));
11252
12553
  try {
11253
12554
  const stateDir = join(cwd, ".omx", "state");
11254
- const sessionId = "sess-stop-stale-session-ralplan";
12555
+ const sessionId = "sess-stop-unscoped-root-no-plan-artifact";
11255
12556
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11256
12557
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11257
12558
  await writeJson(join(stateDir, "skill-active-state.json"), {
11258
12559
  active: false,
11259
12560
  skill: "ralplan",
11260
- phase: "reviewing",
12561
+ phase: "complete",
11261
12562
  active_skills: [],
11262
12563
  });
11263
12564
  await writeJson(join(stateDir, "ralplan-state.json"), {
11264
12565
  active: false,
11265
12566
  mode: "ralplan",
11266
12567
  current_phase: "complete",
11267
- session_id: sessionId,
12568
+ planning_complete: true,
12569
+ updated_at: "2026-06-21T08:05:00.000Z",
11268
12570
  });
11269
12571
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11270
12572
  active: true,
@@ -11283,6 +12585,7 @@ exit 0
11283
12585
  mode: "ralplan",
11284
12586
  current_phase: "planning",
11285
12587
  session_id: sessionId,
12588
+ updated_at: "2026-06-21T08:00:00.000Z",
11286
12589
  });
11287
12590
 
11288
12591
  const result = await dispatchCodexNativeHook(
@@ -11295,32 +12598,83 @@ exit 0
11295
12598
  );
11296
12599
 
11297
12600
  assert.equal(result.omxEventName, "stop");
11298
- assert.equal(result.outputJson, null);
12601
+ assert.equal(result.outputJson?.decision, "block");
12602
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
11299
12603
  } finally {
11300
12604
  await rm(cwd, { recursive: true, force: true });
11301
12605
  }
11302
12606
  });
11303
12607
 
11304
- it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
11305
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
12608
+ it("does not loop when newer unscoped root ralplan complete state shadows stale session planning", async () => {
12609
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-newer-root-complete-"));
11306
12610
  try {
11307
12611
  const stateDir = join(cwd, ".omx", "state");
11308
- const sessionId = "sess-stop-current-active-ralplan";
12612
+ const sessionId = "sess-stop-stale-session-ralplan-newer-root";
11309
12613
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11310
12614
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12615
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
12616
+ active: true,
12617
+ skill: "ralplan",
12618
+ phase: "planning",
12619
+ session_id: sessionId,
12620
+ active_skills: [{
12621
+ skill: "ralplan",
12622
+ phase: "planning",
12623
+ active: true,
12624
+ session_id: sessionId,
12625
+ }],
12626
+ });
12627
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12628
+ active: true,
12629
+ mode: "ralplan",
12630
+ current_phase: "planning",
12631
+ session_id: sessionId,
12632
+ updated_at: "2026-06-21T08:00:00.000Z",
12633
+ });
12634
+ await mkdir(join(cwd, ".omx", "plans"), { recursive: true });
12635
+ await writeFile(join(cwd, ".omx", "plans", "prd-issue-2923.md"), "# PRD\n");
12636
+ await writeFile(join(cwd, ".omx", "plans", "test-spec-issue-2923.md"), "# Test spec\n");
11311
12637
  await writeJson(join(stateDir, "skill-active-state.json"), {
11312
12638
  active: false,
11313
12639
  skill: "ralplan",
11314
12640
  phase: "complete",
11315
- session_id: "sess-stop-old-ralplan",
11316
12641
  active_skills: [],
11317
12642
  });
11318
12643
  await writeJson(join(stateDir, "ralplan-state.json"), {
11319
12644
  active: false,
11320
12645
  mode: "ralplan",
11321
12646
  current_phase: "complete",
11322
- session_id: "sess-stop-old-ralplan",
12647
+ cwd,
12648
+ planning_complete: true,
12649
+ latest_plan_path: ".omx/plans/prd-issue-2923.md",
12650
+ updated_at: "2026-06-21T08:05:00.000Z",
11323
12651
  });
12652
+
12653
+ const result = await dispatchCodexNativeHook(
12654
+ {
12655
+ hook_event_name: "Stop",
12656
+ cwd,
12657
+ session_id: sessionId,
12658
+ tool_name: "apply_patch",
12659
+ },
12660
+ { cwd },
12661
+ );
12662
+
12663
+ assert.equal(result.omxEventName, "stop");
12664
+ assert.equal(result.outputJson, null);
12665
+ } finally {
12666
+ await rm(cwd, { recursive: true, force: true });
12667
+ }
12668
+ });
12669
+
12670
+ it("keeps blocking current session ralplan when newer unscoped root completion belongs to another worktree", async () => {
12671
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-cross-worktree-root-complete-"));
12672
+ try {
12673
+ const otherWorktree = join(tmpdir(), "omx-native-hook-other-worktree-2925");
12674
+ const stateDir = join(cwd, ".omx", "state");
12675
+ const sessionId = "sess-stop-ralplan-cross-worktree-root";
12676
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12677
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11324
12678
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11325
12679
  active: true,
11326
12680
  skill: "ralplan",
@@ -11338,6 +12692,23 @@ exit 0
11338
12692
  mode: "ralplan",
11339
12693
  current_phase: "planning",
11340
12694
  session_id: sessionId,
12695
+ cwd,
12696
+ updated_at: "2026-06-21T08:00:00.000Z",
12697
+ });
12698
+ await writeJson(join(stateDir, "skill-active-state.json"), {
12699
+ active: false,
12700
+ skill: "ralplan",
12701
+ phase: "complete",
12702
+ active_skills: [],
12703
+ });
12704
+ await writeJson(join(stateDir, "ralplan-state.json"), {
12705
+ active: false,
12706
+ mode: "ralplan",
12707
+ current_phase: "complete",
12708
+ cwd: otherWorktree,
12709
+ planning_complete: true,
12710
+ latest_plan_path: ".omx/plans/prd-issue-2925.md",
12711
+ updated_at: "2026-06-21T08:05:00.000Z",
11341
12712
  });
11342
12713
 
11343
12714
  const result = await dispatchCodexNativeHook(
@@ -11345,6 +12716,7 @@ exit 0
11345
12716
  hook_event_name: "Stop",
11346
12717
  cwd,
11347
12718
  session_id: sessionId,
12719
+ tool_name: "apply_patch",
11348
12720
  },
11349
12721
  { cwd },
11350
12722
  );
@@ -11357,24 +12729,13 @@ exit 0
11357
12729
  }
11358
12730
  });
11359
12731
 
11360
- it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
11361
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
12732
+ it("keeps blocking current session ralplan when newer unscoped root completion lacks project context", async () => {
12733
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-root-complete-no-cwd-"));
11362
12734
  try {
11363
12735
  const stateDir = join(cwd, ".omx", "state");
11364
- const sessionId = "sess-stop-unscoped-root-current-active";
12736
+ const sessionId = "sess-stop-ralplan-root-complete-no-cwd";
11365
12737
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11366
12738
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11367
- await writeJson(join(stateDir, "skill-active-state.json"), {
11368
- active: false,
11369
- skill: "ralplan",
11370
- phase: "complete",
11371
- active_skills: [],
11372
- });
11373
- await writeJson(join(stateDir, "ralplan-state.json"), {
11374
- active: false,
11375
- mode: "ralplan",
11376
- current_phase: "complete",
11377
- });
11378
12739
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11379
12740
  active: true,
11380
12741
  skill: "ralplan",
@@ -11392,6 +12753,22 @@ exit 0
11392
12753
  mode: "ralplan",
11393
12754
  current_phase: "planning",
11394
12755
  session_id: sessionId,
12756
+ cwd,
12757
+ updated_at: "2026-06-21T08:00:00.000Z",
12758
+ });
12759
+ await writeJson(join(stateDir, "skill-active-state.json"), {
12760
+ active: false,
12761
+ skill: "ralplan",
12762
+ phase: "complete",
12763
+ active_skills: [],
12764
+ });
12765
+ await writeJson(join(stateDir, "ralplan-state.json"), {
12766
+ active: false,
12767
+ mode: "ralplan",
12768
+ current_phase: "complete",
12769
+ planning_complete: true,
12770
+ latest_plan_path: ".omx/plans/prd-issue-2925.md",
12771
+ updated_at: "2026-06-21T08:05:00.000Z",
11395
12772
  });
11396
12773
 
11397
12774
  const result = await dispatchCodexNativeHook(
@@ -11399,6 +12776,7 @@ exit 0
11399
12776
  hook_event_name: "Stop",
11400
12777
  cwd,
11401
12778
  session_id: sessionId,
12779
+ tool_name: "apply_patch",
11402
12780
  },
11403
12781
  { cwd },
11404
12782
  );
@@ -11435,6 +12813,7 @@ exit 0
11435
12813
  mode: "ralplan",
11436
12814
  current_phase: "complete",
11437
12815
  session_id: sessionId,
12816
+ cwd,
11438
12817
  });
11439
12818
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11440
12819
  active: true,