oh-my-codex 0.18.12 → 0.18.14

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 (317) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +38 -11
  4. package/dist/agents/__tests__/native-config.test.js +25 -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 +5 -1
  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 +936 -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 +62 -18
  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__/codex-plugin-layout.test.js +30 -0
  36. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  37. package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts +2 -0
  38. package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts.map +1 -0
  39. package/dist/cli/__tests__/doctor-artifact-ownership.test.js +158 -0
  40. package/dist/cli/__tests__/doctor-artifact-ownership.test.js.map +1 -0
  41. package/dist/cli/__tests__/doctor-invalid-config.test.js +35 -0
  42. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  43. package/dist/cli/__tests__/doctor-spark-routing.test.js +13 -0
  44. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
  45. package/dist/cli/__tests__/doctor-warning-copy.test.js +1 -0
  46. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  47. package/dist/cli/__tests__/index.test.js +59 -1
  48. package/dist/cli/__tests__/index.test.js.map +1 -1
  49. package/dist/cli/__tests__/nested-help-routing.test.js +4 -0
  50. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  51. package/dist/cli/__tests__/package-bin-contract.test.js +7 -1
  52. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  53. package/dist/cli/__tests__/performance-goal.test.js +22 -0
  54. package/dist/cli/__tests__/performance-goal.test.js.map +1 -1
  55. package/dist/cli/__tests__/resume.test.js +313 -1
  56. package/dist/cli/__tests__/resume.test.js.map +1 -1
  57. package/dist/cli/__tests__/session-scoped-runtime.test.js +162 -0
  58. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  59. package/dist/cli/__tests__/session-search-help.test.js +3 -2
  60. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  61. package/dist/cli/__tests__/session-search.test.js +100 -2
  62. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  63. package/dist/cli/__tests__/setup-agents-overwrite.test.js +35 -1
  64. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  65. package/dist/cli/__tests__/setup-install-mode.test.js +39 -5
  66. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  67. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +74 -0
  68. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  69. package/dist/cli/__tests__/setup-scope.test.js +45 -0
  70. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  71. package/dist/cli/__tests__/team.test.js +17 -3
  72. package/dist/cli/__tests__/team.test.js.map +1 -1
  73. package/dist/cli/__tests__/ultragoal.test.js +37 -0
  74. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  75. package/dist/cli/__tests__/update.test.js +6 -2
  76. package/dist/cli/__tests__/update.test.js.map +1 -1
  77. package/dist/cli/autoresearch-goal.d.ts.map +1 -1
  78. package/dist/cli/autoresearch-goal.js +2 -1
  79. package/dist/cli/autoresearch-goal.js.map +1 -1
  80. package/dist/cli/doctor.d.ts +22 -0
  81. package/dist/cli/doctor.d.ts.map +1 -1
  82. package/dist/cli/doctor.js +272 -5
  83. package/dist/cli/doctor.js.map +1 -1
  84. package/dist/cli/index.d.ts +10 -2
  85. package/dist/cli/index.d.ts.map +1 -1
  86. package/dist/cli/index.js +274 -11
  87. package/dist/cli/index.js.map +1 -1
  88. package/dist/cli/performance-goal.d.ts.map +1 -1
  89. package/dist/cli/performance-goal.js +4 -2
  90. package/dist/cli/performance-goal.js.map +1 -1
  91. package/dist/cli/plugin-marketplace.d.ts +1 -0
  92. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  93. package/dist/cli/plugin-marketplace.js +67 -5
  94. package/dist/cli/plugin-marketplace.js.map +1 -1
  95. package/dist/cli/project-runtime-codex-homes.d.ts +8 -0
  96. package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -0
  97. package/dist/cli/project-runtime-codex-homes.js +134 -0
  98. package/dist/cli/project-runtime-codex-homes.js.map +1 -0
  99. package/dist/cli/session-search.d.ts.map +1 -1
  100. package/dist/cli/session-search.js +8 -1
  101. package/dist/cli/session-search.js.map +1 -1
  102. package/dist/cli/setup.d.ts +1 -0
  103. package/dist/cli/setup.d.ts.map +1 -1
  104. package/dist/cli/setup.js +183 -10
  105. package/dist/cli/setup.js.map +1 -1
  106. package/dist/cli/ultragoal.d.ts.map +1 -1
  107. package/dist/cli/ultragoal.js +5 -1
  108. package/dist/cli/ultragoal.js.map +1 -1
  109. package/dist/cli/update.d.ts.map +1 -1
  110. package/dist/cli/update.js +5 -0
  111. package/dist/cli/update.js.map +1 -1
  112. package/dist/config/__tests__/codex-hooks.test.js +38 -24
  113. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  114. package/dist/config/__tests__/models.test.js +21 -1
  115. package/dist/config/__tests__/models.test.js.map +1 -1
  116. package/dist/config/codex-hooks.d.ts +6 -0
  117. package/dist/config/codex-hooks.d.ts.map +1 -1
  118. package/dist/config/codex-hooks.js +34 -49
  119. package/dist/config/codex-hooks.js.map +1 -1
  120. package/dist/config/models.d.ts +5 -0
  121. package/dist/config/models.d.ts.map +1 -1
  122. package/dist/config/models.js +23 -0
  123. package/dist/config/models.js.map +1 -1
  124. package/dist/goal-workflows/codex-goal-snapshot.d.ts +2 -0
  125. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  126. package/dist/goal-workflows/codex-goal-snapshot.js +10 -0
  127. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  128. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +7 -0
  129. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  130. package/dist/hooks/__tests__/keyword-detector.test.js +46 -0
  131. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  132. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +100 -0
  133. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  134. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +75 -0
  135. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  136. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +225 -0
  137. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  138. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +75 -0
  139. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  140. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +200 -0
  141. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  142. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +187 -12
  143. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  144. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +225 -0
  145. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  146. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  147. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  148. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +75 -0
  149. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  150. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  151. package/dist/hooks/keyword-detector.js +4 -0
  152. package/dist/hooks/keyword-detector.js.map +1 -1
  153. package/dist/hud/__tests__/reconcile.test.js +137 -2
  154. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  155. package/dist/hud/__tests__/state.test.js +107 -0
  156. package/dist/hud/__tests__/state.test.js.map +1 -1
  157. package/dist/hud/__tests__/tmux.test.js +17 -0
  158. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  159. package/dist/hud/reconcile.d.ts.map +1 -1
  160. package/dist/hud/reconcile.js +21 -7
  161. package/dist/hud/reconcile.js.map +1 -1
  162. package/dist/hud/render.d.ts.map +1 -1
  163. package/dist/hud/render.js +9 -0
  164. package/dist/hud/render.js.map +1 -1
  165. package/dist/hud/state.d.ts.map +1 -1
  166. package/dist/hud/state.js +43 -5
  167. package/dist/hud/state.js.map +1 -1
  168. package/dist/hud/types.d.ts +6 -0
  169. package/dist/hud/types.d.ts.map +1 -1
  170. package/dist/hud/types.js.map +1 -1
  171. package/dist/modes/__tests__/base-autopilot-gates.test.js +48 -0
  172. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  173. package/dist/modes/base.d.ts.map +1 -1
  174. package/dist/modes/base.js +1 -0
  175. package/dist/modes/base.js.map +1 -1
  176. package/dist/performance-goal/artifacts.d.ts.map +1 -1
  177. package/dist/performance-goal/artifacts.js +10 -1
  178. package/dist/performance-goal/artifacts.js.map +1 -1
  179. package/dist/pipeline/__tests__/orchestrator.test.js +125 -0
  180. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  181. package/dist/pipeline/__tests__/stages.test.js +109 -0
  182. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  183. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  184. package/dist/pipeline/orchestrator.js +7 -0
  185. package/dist/pipeline/orchestrator.js.map +1 -1
  186. package/dist/ralplan/__tests__/consensus-gate.test.js +552 -1
  187. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  188. package/dist/ralplan/consensus-gate.d.ts +25 -0
  189. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  190. package/dist/ralplan/consensus-gate.js +289 -75
  191. package/dist/ralplan/consensus-gate.js.map +1 -1
  192. package/dist/scripts/__tests__/codex-native-hook.test.js +1149 -47
  193. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  194. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  195. package/dist/scripts/codex-native-hook.js +542 -33
  196. package/dist/scripts/codex-native-hook.js.map +1 -1
  197. package/dist/scripts/codex-native-pre-post.d.ts +1 -0
  198. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  199. package/dist/scripts/codex-native-pre-post.js +130 -0
  200. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  201. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  202. package/dist/scripts/notify-hook/team-tmux-guard.js +87 -8
  203. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  204. package/dist/scripts/notify-hook/tmux-injection.js +1 -1
  205. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  206. package/dist/scripts/run-compiled-ci.d.ts +2 -0
  207. package/dist/scripts/run-compiled-ci.d.ts.map +1 -0
  208. package/dist/scripts/run-compiled-ci.js +77 -0
  209. package/dist/scripts/run-compiled-ci.js.map +1 -0
  210. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  211. package/dist/scripts/sync-plugin-mirror.js +1 -0
  212. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  213. package/dist/session-history/__tests__/search.test.js +166 -0
  214. package/dist/session-history/__tests__/search.test.js.map +1 -1
  215. package/dist/session-history/search.d.ts +7 -0
  216. package/dist/session-history/search.d.ts.map +1 -1
  217. package/dist/session-history/search.js +99 -25
  218. package/dist/session-history/search.js.map +1 -1
  219. package/dist/sidecar/__tests__/collector.test.js +60 -0
  220. package/dist/sidecar/__tests__/collector.test.js.map +1 -1
  221. package/dist/sidecar/collector.d.ts.map +1 -1
  222. package/dist/sidecar/collector.js +3 -6
  223. package/dist/sidecar/collector.js.map +1 -1
  224. package/dist/state/__tests__/operations.test.js +107 -3
  225. package/dist/state/__tests__/operations.test.js.map +1 -1
  226. package/dist/state/operations.d.ts.map +1 -1
  227. package/dist/state/operations.js +57 -2
  228. package/dist/state/operations.js.map +1 -1
  229. package/dist/team/__tests__/delivery-e2e-smoke.test.js +22 -1
  230. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  231. package/dist/team/__tests__/model-contract.test.js +57 -1
  232. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  233. package/dist/team/__tests__/runtime.test.js +1468 -110
  234. package/dist/team/__tests__/runtime.test.js.map +1 -1
  235. package/dist/team/__tests__/scaling.test.js +63 -0
  236. package/dist/team/__tests__/scaling.test.js.map +1 -1
  237. package/dist/team/__tests__/state.test.js +24 -1
  238. package/dist/team/__tests__/state.test.js.map +1 -1
  239. package/dist/team/__tests__/tmux-session.test.js +512 -0
  240. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  241. package/dist/team/__tests__/worker-bootstrap.test.js +42 -9
  242. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  243. package/dist/team/model-contract.d.ts +16 -0
  244. package/dist/team/model-contract.d.ts.map +1 -1
  245. package/dist/team/model-contract.js +41 -1
  246. package/dist/team/model-contract.js.map +1 -1
  247. package/dist/team/runtime.d.ts.map +1 -1
  248. package/dist/team/runtime.js +116 -30
  249. package/dist/team/runtime.js.map +1 -1
  250. package/dist/team/scaling.d.ts.map +1 -1
  251. package/dist/team/scaling.js +9 -1
  252. package/dist/team/scaling.js.map +1 -1
  253. package/dist/team/state/types.d.ts +2 -0
  254. package/dist/team/state/types.d.ts.map +1 -1
  255. package/dist/team/state/types.js.map +1 -1
  256. package/dist/team/state.d.ts +3 -0
  257. package/dist/team/state.d.ts.map +1 -1
  258. package/dist/team/state.js +20 -0
  259. package/dist/team/state.js.map +1 -1
  260. package/dist/team/tmux-session.d.ts +38 -3
  261. package/dist/team/tmux-session.d.ts.map +1 -1
  262. package/dist/team/tmux-session.js +275 -36
  263. package/dist/team/tmux-session.js.map +1 -1
  264. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  265. package/dist/team/worker-bootstrap.js +26 -1
  266. package/dist/team/worker-bootstrap.js.map +1 -1
  267. package/dist/ultragoal/__tests__/artifacts.test.js +238 -0
  268. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  269. package/dist/ultragoal/__tests__/docs-contract.test.js +3 -0
  270. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  271. package/dist/ultragoal/artifacts.d.ts +15 -0
  272. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  273. package/dist/ultragoal/artifacts.js +167 -4
  274. package/dist/ultragoal/artifacts.js.map +1 -1
  275. package/dist/utils/__tests__/agents-md.test.js +39 -1
  276. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  277. package/dist/utils/__tests__/agents-model-table.test.js +28 -0
  278. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  279. package/dist/utils/agents-md.d.ts +4 -0
  280. package/dist/utils/agents-md.d.ts.map +1 -1
  281. package/dist/utils/agents-md.js +25 -0
  282. package/dist/utils/agents-md.js.map +1 -1
  283. package/dist/utils/agents-model-table.d.ts +7 -3
  284. package/dist/utils/agents-model-table.d.ts.map +1 -1
  285. package/dist/utils/agents-model-table.js +16 -8
  286. package/dist/utils/agents-model-table.js.map +1 -1
  287. package/dist/verification/__tests__/ci-rust-gates.test.js +8 -2
  288. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  289. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +71 -3
  290. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  291. package/package.json +2 -2
  292. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  293. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +59 -8
  294. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +9 -5
  295. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +3 -0
  296. package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +3 -0
  297. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  298. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +22 -4
  299. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +2 -2
  300. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +61 -0
  301. package/skills/autopilot/SKILL.md +9 -5
  302. package/skills/autoresearch-goal/SKILL.md +3 -0
  303. package/skills/ecomode/SKILL.md +1 -1
  304. package/skills/ecomode/references/agent-tiers.md +61 -0
  305. package/skills/performance-goal/SKILL.md +3 -0
  306. package/skills/ralplan/SKILL.md +1 -1
  307. package/skills/ultragoal/SKILL.md +22 -4
  308. package/skills/ultrawork/SKILL.md +2 -2
  309. package/skills/ultrawork/references/agent-tiers.md +61 -0
  310. package/src/scripts/__tests__/codex-native-hook.test.ts +1417 -119
  311. package/src/scripts/codex-native-hook.ts +597 -40
  312. package/src/scripts/codex-native-pre-post.ts +137 -0
  313. package/src/scripts/notify-hook/team-tmux-guard.ts +92 -8
  314. package/src/scripts/notify-hook/tmux-injection.ts +1 -1
  315. package/src/scripts/run-compiled-ci.ts +85 -0
  316. package/src/scripts/sync-plugin-mirror.ts +1 -0
  317. 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 {
@@ -1464,36 +1745,46 @@ describe("codex native hook dispatch", () => {
1464
1745
  }
1465
1746
  });
1466
1747
 
1467
- it("keeps a self-parented native role thread as subagent evidence", async () => {
1468
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
1748
+ it("suppresses child-agent SessionStart and Stop before the canonical leader session is reconciled (#2831)", async () => {
1749
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-no-canonical-"));
1750
+ const originalCodexHome = process.env.CODEX_HOME;
1469
1751
  try {
1470
- const stateDir = join(cwd, ".omx", "state");
1471
- const canonicalSessionId = "omx-autopilot-session";
1472
- const nativeRoleThreadId = "codex-architect-thread";
1473
- await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1474
- await writeSessionStart(cwd, canonicalSessionId, {
1475
- nativeSessionId: nativeRoleThreadId,
1752
+ process.env.CODEX_HOME = join(cwd, "codex-home");
1753
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
1754
+ notifications: {
1755
+ enabled: true,
1756
+ verbosity: "session",
1757
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
1758
+ },
1476
1759
  });
1477
-
1478
- const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
1760
+ const stateDir = join(cwd, ".omx", "state");
1761
+ const leaderNativeSessionId = "codex-leader-thread-no-canonical";
1762
+ const childNativeSessionId = "codex-child-thread-no-canonical";
1763
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
1764
+ await writeFile(
1765
+ join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
1766
+ [
1767
+ "import { appendFileSync } from 'node:fs';",
1768
+ "export async function onHookEvent(event) {",
1769
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
1770
+ "}",
1771
+ ].join("\n"),
1772
+ );
1773
+ const transcriptPath = join(cwd, "no-canonical-subagent-rollout.jsonl");
1479
1774
  await writeFile(
1480
1775
  transcriptPath,
1481
1776
  `${JSON.stringify({
1482
1777
  type: "session_meta",
1483
1778
  payload: {
1484
- id: nativeRoleThreadId,
1779
+ id: childNativeSessionId,
1485
1780
  source: {
1486
1781
  subagent: {
1487
1782
  thread_spawn: {
1488
- parent_thread_id: nativeRoleThreadId,
1489
- depth: 1,
1490
- agent_nickname: "Architect",
1491
- agent_role: "architect",
1783
+ parent_thread_id: leaderNativeSessionId,
1784
+ agent_role: "explorer",
1492
1785
  },
1493
1786
  },
1494
1787
  },
1495
- agent_nickname: "Architect",
1496
- agent_role: "architect",
1497
1788
  },
1498
1789
  })}\n`,
1499
1790
  );
@@ -1502,12 +1793,23 @@ describe("codex native hook dispatch", () => {
1502
1793
  {
1503
1794
  hook_event_name: "SessionStart",
1504
1795
  cwd,
1505
- session_id: nativeRoleThreadId,
1796
+ session_id: childNativeSessionId,
1506
1797
  transcript_path: transcriptPath,
1507
1798
  },
1508
1799
  { cwd, sessionOwnerPid: process.pid },
1509
1800
  );
1510
1801
 
1802
+ assert.equal(
1803
+ existsSync(join(cwd, "hook-events.jsonl")),
1804
+ false,
1805
+ "child SessionStart must be suppressed even before the canonical leader session is reconciled",
1806
+ );
1807
+ assert.equal(
1808
+ existsSync(join(stateDir, "session.json")),
1809
+ false,
1810
+ "child SessionStart must not be promoted into a root/leader session",
1811
+ );
1812
+
1511
1813
  const tracking = JSON.parse(
1512
1814
  await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
1513
1815
  ) as {
@@ -1516,27 +1818,156 @@ describe("codex native hook dispatch", () => {
1516
1818
  threads?: Record<string, { kind?: string; mode?: string }>;
1517
1819
  }>;
1518
1820
  };
1519
- assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
1520
- assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
1521
- assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
1821
+ assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
1822
+ assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
1823
+
1824
+ await dispatchCodexNativeHook(
1825
+ {
1826
+ hook_event_name: "Stop",
1827
+ cwd,
1828
+ session_id: childNativeSessionId,
1829
+ thread_id: childNativeSessionId,
1830
+ turn_id: "no-canonical-child-stop-turn",
1831
+ },
1832
+ { cwd },
1833
+ );
1834
+
1835
+ assert.equal(
1836
+ existsSync(join(cwd, "hook-events.jsonl")),
1837
+ false,
1838
+ "child Stop must be suppressed when the start was recognized as subagent-scoped",
1839
+ );
1522
1840
  } finally {
1841
+ if (originalCodexHome === undefined) {
1842
+ delete process.env.CODEX_HOME;
1843
+ } else {
1844
+ process.env.CODEX_HOME = originalCodexHome;
1845
+ }
1523
1846
  await rm(cwd, { recursive: true, force: true });
1524
1847
  }
1525
1848
  });
1526
1849
 
1527
- it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
1528
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
1850
+ it("preserves root/leader SessionStart dispatch at session verbosity (#2831)", async () => {
1851
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-root-session-start-preserved-"));
1852
+ const originalCodexHome = process.env.CODEX_HOME;
1529
1853
  try {
1530
- const stateDir = join(cwd, ".omx", "state");
1531
- const canonicalSessionId = "omx-leader-session-a";
1532
- const leaderNativeSessionId = "codex-leader-thread-a";
1533
- const unrelatedParentNativeSessionId = "codex-leader-thread-b";
1534
- const childNativeSessionId = "codex-child-thread-b";
1535
- await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1536
- await writeSessionStart(cwd, canonicalSessionId, {
1537
- nativeSessionId: leaderNativeSessionId,
1538
- });
1539
- await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
1854
+ process.env.CODEX_HOME = join(cwd, "codex-home");
1855
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
1856
+ notifications: {
1857
+ enabled: true,
1858
+ verbosity: "session",
1859
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
1860
+ },
1861
+ });
1862
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
1863
+ await writeFile(
1864
+ join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
1865
+ [
1866
+ "import { appendFileSync } from 'node:fs';",
1867
+ "export async function onHookEvent(event) {",
1868
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
1869
+ "}",
1870
+ ].join("\n"),
1871
+ );
1872
+
1873
+ await dispatchCodexNativeHook(
1874
+ {
1875
+ hook_event_name: "SessionStart",
1876
+ cwd,
1877
+ session_id: "codex-root-thread-preserved",
1878
+ },
1879
+ { cwd, sessionOwnerPid: process.pid },
1880
+ );
1881
+
1882
+ const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
1883
+ assert.match(
1884
+ hookEvents,
1885
+ /"event":"session-start"/,
1886
+ "root/leader SessionStart must still dispatch at session verbosity",
1887
+ );
1888
+ } finally {
1889
+ if (originalCodexHome === undefined) {
1890
+ delete process.env.CODEX_HOME;
1891
+ } else {
1892
+ process.env.CODEX_HOME = originalCodexHome;
1893
+ }
1894
+ await rm(cwd, { recursive: true, force: true });
1895
+ }
1896
+ });
1897
+
1898
+ it("keeps a self-parented native role thread as subagent evidence", async () => {
1899
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
1900
+ try {
1901
+ const stateDir = join(cwd, ".omx", "state");
1902
+ const canonicalSessionId = "omx-autopilot-session";
1903
+ const nativeRoleThreadId = "codex-architect-thread";
1904
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1905
+ await writeSessionStart(cwd, canonicalSessionId, {
1906
+ nativeSessionId: nativeRoleThreadId,
1907
+ });
1908
+
1909
+ const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
1910
+ await writeFile(
1911
+ transcriptPath,
1912
+ `${JSON.stringify({
1913
+ type: "session_meta",
1914
+ payload: {
1915
+ id: nativeRoleThreadId,
1916
+ source: {
1917
+ subagent: {
1918
+ thread_spawn: {
1919
+ parent_thread_id: nativeRoleThreadId,
1920
+ depth: 1,
1921
+ agent_nickname: "Architect",
1922
+ agent_role: "architect",
1923
+ },
1924
+ },
1925
+ },
1926
+ agent_nickname: "Architect",
1927
+ agent_role: "architect",
1928
+ },
1929
+ })}\n`,
1930
+ );
1931
+
1932
+ await dispatchCodexNativeHook(
1933
+ {
1934
+ hook_event_name: "SessionStart",
1935
+ cwd,
1936
+ session_id: nativeRoleThreadId,
1937
+ transcript_path: transcriptPath,
1938
+ },
1939
+ { cwd, sessionOwnerPid: process.pid },
1940
+ );
1941
+
1942
+ const tracking = JSON.parse(
1943
+ await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
1944
+ ) as {
1945
+ sessions?: Record<string, {
1946
+ leader_thread_id?: string;
1947
+ threads?: Record<string, { kind?: string; mode?: string }>;
1948
+ }>;
1949
+ };
1950
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
1951
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
1952
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
1953
+ } finally {
1954
+ await rm(cwd, { recursive: true, force: true });
1955
+ }
1956
+ });
1957
+
1958
+ it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
1959
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
1960
+ try {
1961
+ const stateDir = join(cwd, ".omx", "state");
1962
+ const canonicalSessionId = "omx-leader-session-a";
1963
+ const leaderNativeSessionId = "codex-leader-thread-a";
1964
+ const unrelatedParentNativeSessionId = "codex-leader-thread-b";
1965
+ const childNativeSessionId = "codex-child-thread-b";
1966
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1967
+ await writeSessionStart(cwd, canonicalSessionId, {
1968
+ nativeSessionId: leaderNativeSessionId,
1969
+ });
1970
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
1540
1971
  active: true,
1541
1972
  mode: "ralph",
1542
1973
  current_phase: "executing",
@@ -2823,6 +3254,64 @@ standardMaxRounds = 15
2823
3254
  }
2824
3255
  });
2825
3256
 
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-"));
3259
+ try {
3260
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
3261
+ version: 1,
3262
+ workflow: "performance-goal",
3263
+ slug: "latency",
3264
+ objective: "Reduce latency",
3265
+ status: "complete",
3266
+ completedAt: "2026-05-20T00:00:00.000Z",
3267
+ });
3268
+
3269
+ const result = await dispatchCodexNativeHook({
3270
+ hook_event_name: "UserPromptSubmit",
3271
+ cwd,
3272
+ session_id: "sess-completed-goal-prompt",
3273
+ thread_id: "thread-completed-goal-prompt",
3274
+ prompt: "Start the next performance goal now",
3275
+ }, { cwd });
3276
+
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);
3282
+ } finally {
3283
+ await rm(cwd, { recursive: true, force: true });
3284
+ }
3285
+ });
3286
+
3287
+ it("blocks Stop when a final answer starts another goal over completed state", async () => {
3288
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-stop-"));
3289
+ try {
3290
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
3291
+ version: 1,
3292
+ aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
3293
+ goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
3294
+ });
3295
+
3296
+ const result = await dispatchCodexNativeHook({
3297
+ hook_event_name: "Stop",
3298
+ cwd,
3299
+ session_id: "sess-completed-goal-stop",
3300
+ thread_id: "thread-completed-goal-stop",
3301
+ last_assistant_message: "Starting another ultragoal now; create_goal payload follows.",
3302
+ }, { cwd });
3303
+
3304
+ const output = JSON.stringify(result.outputJson);
3305
+ assert.equal(result.outputJson?.decision, "block");
3306
+ assert.equal(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
3307
+ assert.match(output, /run \/goal clear/);
3308
+ assert.match(output, /before calling create_goal/);
3309
+ assert.match(output, /hooks only nudge and must not mutate Codex goal state/);
3310
+ } finally {
3311
+ await rm(cwd, { recursive: true, force: true });
3312
+ }
3313
+ });
3314
+
2826
3315
  it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
2827
3316
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
2828
3317
  try {
@@ -5903,161 +6392,512 @@ exit 0
5903
6392
  }
5904
6393
  });
5905
6394
 
5906
- it("allows deep-interview apply_patch artifact writes from freeform patch text while blocking outside paths", async () => {
5907
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-apply-patch-"));
6395
+ it("allows read-only diagnostics mentioning apply_patch while deep-interview blocks real apply_patch invocations", async () => {
6396
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-grep-apply-patch-"));
5908
6397
  try {
5909
6398
  const stateDir = join(cwd, ".omx", "state");
5910
- const sessionDir = join(stateDir, "sessions", "sess-di-apply-patch");
6399
+ const sessionDir = join(stateDir, "sessions", "sess-di-grep");
5911
6400
  await mkdir(sessionDir, { recursive: true });
5912
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-apply-patch", cwd });
6401
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-grep", cwd });
5913
6402
  await writeJson(join(sessionDir, "skill-active-state.json"), {
5914
6403
  version: 1,
5915
6404
  active: true,
5916
6405
  skill: "deep-interview",
5917
6406
  phase: "planning",
5918
- session_id: "sess-di-apply-patch",
5919
- active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-apply-patch" }],
6407
+ session_id: "sess-di-grep",
6408
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-grep" }],
5920
6409
  });
5921
6410
  await writeJson(join(sessionDir, "deep-interview-state.json"), {
5922
6411
  active: true,
5923
6412
  mode: "deep-interview",
5924
6413
  current_phase: "intent-first",
5925
- session_id: "sess-di-apply-patch",
6414
+ session_id: "sess-di-grep",
5926
6415
  });
5927
6416
 
5928
- const allowedAddFile = await dispatchCodexNativeHook(
6417
+ const allowedGrep = await dispatchCodexNativeHook(
5929
6418
  {
5930
6419
  hook_event_name: "PreToolUse",
5931
6420
  cwd,
5932
- session_id: "sess-di-apply-patch",
5933
- tool_name: "apply_patch",
5934
- tool_use_id: "tool-di-apply-patch-add",
5935
- tool_input: {
5936
- input: "*** Begin Patch\n*** Add File: .omx/context/findings.md\n+# Findings\n*** End Patch\n",
5937
- },
6421
+ session_id: "sess-di-grep",
6422
+ tool_name: "Bash",
6423
+ tool_use_id: "tool-di-grep",
6424
+ tool_input: { command: 'grep -n "apply_patch" dist/scripts/codex-native-hook.js' },
5938
6425
  },
5939
6426
  { cwd },
5940
6427
  );
5941
- assert.equal(allowedAddFile.outputJson, null);
6428
+ assert.equal(allowedGrep.outputJson, null);
5942
6429
 
5943
- const allowedUpdateFile = await dispatchCodexNativeHook(
6430
+ const allowedSubshellGrep = await dispatchCodexNativeHook(
5944
6431
  {
5945
6432
  hook_event_name: "PreToolUse",
5946
6433
  cwd,
5947
- session_id: "sess-di-apply-patch",
5948
- tool_name: "ApplyPatch",
5949
- tool_use_id: "tool-di-apply-patch-update",
5950
- tool_input: {
5951
- input: "*** Begin Patch\n*** Update File: .omx/specs/deep-interview-demo.md\n@@\n-old\n+new\n*** End Patch\n",
5952
- },
6434
+ session_id: "sess-di-grep",
6435
+ tool_name: "Bash",
6436
+ tool_use_id: "tool-di-grep-subshell",
6437
+ tool_input: { command: '(grep -n "apply_patch" dist/scripts/codex-native-hook.js)' },
5953
6438
  },
5954
6439
  { cwd },
5955
6440
  );
5956
- assert.equal(allowedUpdateFile.outputJson, null);
6441
+ assert.equal(allowedSubshellGrep.outputJson, null);
5957
6442
 
5958
- const allowedStateWrite = await dispatchCodexNativeHook(
6443
+ // Double-quoted spans that merely mention the literal token, expand a
6444
+ // parameter, or run a substitution that is not `apply_patch` stay allowed
6445
+ // — the quoted-substitution fix must not over-block read-only diagnostics.
6446
+ const allowedQuotedMention = await dispatchCodexNativeHook(
5959
6447
  {
5960
6448
  hook_event_name: "PreToolUse",
5961
6449
  cwd,
5962
- session_id: "sess-di-apply-patch",
5963
- tool_name: "apply_patch",
5964
- tool_use_id: "tool-di-apply-patch-state",
5965
- tool_input: {
5966
- input: "*** Begin Patch\n*** Add File: .omx/state/deep-interview-notes.json\n+{}\n*** End Patch\n",
5967
- },
6450
+ session_id: "sess-di-grep",
6451
+ tool_name: "Bash",
6452
+ tool_use_id: "tool-di-grep-quoted-mention",
6453
+ tool_input: { command: 'echo "${apply_patch} $(echo apply_patch)"' },
5968
6454
  },
5969
6455
  { cwd },
5970
6456
  );
5971
- assert.equal(allowedStateWrite.outputJson, null);
6457
+ assert.equal(allowedQuotedMention.outputJson, null);
5972
6458
 
5973
- const blockedOutsidePath = await dispatchCodexNativeHook(
6459
+ const blockedApplyPatch = await dispatchCodexNativeHook(
5974
6460
  {
5975
6461
  hook_event_name: "PreToolUse",
5976
6462
  cwd,
5977
- session_id: "sess-di-apply-patch",
5978
- tool_name: "apply_patch",
5979
- tool_use_id: "tool-di-apply-patch-outside",
5980
- tool_input: {
5981
- input: "*** Begin Patch\n*** Add File: src/implementation.ts\n+export const x = 1;\n*** End Patch\n",
5982
- },
6463
+ session_id: "sess-di-grep",
6464
+ tool_name: "Bash",
6465
+ tool_use_id: "tool-di-apply-patch-invoke",
6466
+ tool_input: { command: "apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
5983
6467
  },
5984
6468
  { cwd },
5985
6469
  );
5986
- assert.equal((blockedOutsidePath.outputJson as { decision?: string } | null)?.decision, "block");
5987
- assert.match(String((blockedOutsidePath.outputJson as { reason?: string } | null)?.reason ?? ""), /Deep-interview is active/);
6470
+ assert.equal((blockedApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
5988
6471
 
5989
- const blockedMixedPaths = await dispatchCodexNativeHook(
6472
+ const blockedEnvAssignmentApplyPatch = await dispatchCodexNativeHook(
5990
6473
  {
5991
6474
  hook_event_name: "PreToolUse",
5992
6475
  cwd,
5993
- session_id: "sess-di-apply-patch",
5994
- tool_name: "apply_patch",
5995
- tool_use_id: "tool-di-apply-patch-mixed",
5996
- tool_input: {
5997
- input: "*** Begin Patch\n*** Add File: .omx/context/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
5998
- },
6476
+ session_id: "sess-di-grep",
6477
+ tool_name: "Bash",
6478
+ tool_use_id: "tool-di-apply-patch-env-assignment",
6479
+ tool_input: { command: "FOO=bar apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
5999
6480
  },
6000
6481
  { cwd },
6001
6482
  );
6002
- assert.equal((blockedMixedPaths.outputJson as { decision?: string } | null)?.decision, "block");
6483
+ assert.equal((blockedEnvAssignmentApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
6003
6484
 
6004
- const blockedUnparseablePatch = await dispatchCodexNativeHook(
6485
+ const blockedEnvWrapperApplyPatch = await dispatchCodexNativeHook(
6005
6486
  {
6006
6487
  hook_event_name: "PreToolUse",
6007
6488
  cwd,
6008
- session_id: "sess-di-apply-patch",
6009
- tool_name: "apply_patch",
6010
- tool_use_id: "tool-di-apply-patch-empty",
6011
- tool_input: { input: "not a recognizable patch" },
6012
- },
6013
- { cwd },
6014
- );
6015
- assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
6489
+ session_id: "sess-di-grep",
6490
+ tool_name: "Bash",
6491
+ tool_use_id: "tool-di-apply-patch-env-wrapper",
6492
+ tool_input: { command: "env FOO=bar apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
6493
+ },
6494
+ { cwd },
6495
+ );
6496
+ assert.equal((blockedEnvWrapperApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
6497
+
6498
+ const heredocBody = "\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF";
6499
+ const blockedRealApplyPatchForms: Array<{ id: string; command: string }> = [
6500
+ { id: "tool-di-apply-patch-env-i", command: `env -i apply_patch <<'EOF'${heredocBody}` },
6501
+ { id: "tool-di-apply-patch-env-unset", command: `env -u FOO apply_patch <<'EOF'${heredocBody}` },
6502
+ { id: "tool-di-apply-patch-env-i-assignment", command: `env -i FOO=bar apply_patch <<'EOF'${heredocBody}` },
6503
+ { id: "tool-di-apply-patch-assignment-env", command: `FOO=bar env apply_patch <<'EOF'${heredocBody}` },
6504
+ { id: "tool-di-apply-patch-exec-env-assignment", command: `exec env FOO=bar apply_patch <<'EOF'${heredocBody}` },
6505
+ { id: "tool-di-apply-patch-absolute-path", command: `/usr/bin/apply_patch <<'EOF'${heredocBody}` },
6506
+ { id: "tool-di-apply-patch-relative-path", command: `./apply_patch <<'EOF'${heredocBody}` },
6507
+ { id: "tool-di-apply-patch-subshell", command: `(apply_patch <<'EOF'${heredocBody}\n)` },
6508
+ { id: "tool-di-apply-patch-subshell-spaced", command: `( apply_patch <<'EOF'${heredocBody}\n)` },
6509
+ { id: "tool-di-apply-patch-double-subshell", command: `((apply_patch <<'EOF'${heredocBody}\n))` },
6510
+ { id: "tool-di-apply-patch-pipe-subshell", command: `true | (apply_patch <<'EOF'${heredocBody}\n)` },
6511
+ { id: "tool-di-apply-patch-subshell-env", command: `(env apply_patch <<'EOF'${heredocBody}\n)` },
6512
+ { id: "tool-di-apply-patch-command-substitution", command: `x=$(apply_patch <<'EOF'${heredocBody}\n)` },
6513
+ { id: "tool-di-apply-patch-brace-group", command: `{ apply_patch <<'EOF'${heredocBody}\n}` },
6514
+ // Command substitution runs even inside double quotes, so quoting the
6515
+ // already-blocked `$(…)` / `` `…` `` form must not bypass the guard.
6516
+ { id: "tool-di-apply-patch-quoted-command-substitution", command: `echo "$(apply_patch <<'EOF'${heredocBody}\n)"` },
6517
+ { id: "tool-di-apply-patch-quoted-backtick", command: `echo "\`apply_patch <<'EOF'${heredocBody}\`"` },
6518
+ { id: "tool-di-apply-patch-quoted-command-substitution-prefixed", command: `echo "patched: $(apply_patch <<'EOF'${heredocBody}\n) done"` },
6519
+ ];
6520
+ for (const form of blockedRealApplyPatchForms) {
6521
+ const blockedForm = await dispatchCodexNativeHook(
6522
+ {
6523
+ hook_event_name: "PreToolUse",
6524
+ cwd,
6525
+ session_id: "sess-di-grep",
6526
+ tool_name: "Bash",
6527
+ tool_use_id: form.id,
6528
+ tool_input: { command: form.command },
6529
+ },
6530
+ { cwd },
6531
+ );
6532
+ assert.equal(
6533
+ (blockedForm.outputJson as { decision?: string } | null)?.decision,
6534
+ "block",
6535
+ `expected deep-interview to block real apply_patch form: ${form.command}`,
6536
+ );
6537
+ }
6016
6538
  } finally {
6017
6539
  await rm(cwd, { recursive: true, force: true });
6018
6540
  }
6019
6541
  });
6020
6542
 
6021
- it("allows Autopilot ralplan planning artifacts while blocking implementation writes", async () => {
6022
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-ralplan-artifact-"));
6543
+ it("allows deep-interview same-command literal variable redirects to artifacts while blocking variable redirects outside them", async () => {
6544
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-var-redirect-"));
6023
6545
  try {
6024
6546
  const stateDir = join(cwd, ".omx", "state");
6025
- const sessionDir = join(stateDir, "sessions", "sess-autopilot-ralplan-artifact");
6547
+ const sessionDir = join(stateDir, "sessions", "sess-di-var-redirect");
6026
6548
  await mkdir(sessionDir, { recursive: true });
6027
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-ralplan-artifact", cwd });
6549
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-var-redirect", cwd });
6028
6550
  await writeJson(join(sessionDir, "skill-active-state.json"), {
6029
6551
  version: 1,
6030
6552
  active: true,
6031
- skill: "autopilot",
6032
- phase: "ralplan",
6033
- session_id: "sess-autopilot-ralplan-artifact",
6034
- thread_id: "thread-autopilot-ralplan-artifact",
6035
- active_skills: [
6036
- {
6037
- skill: "autopilot",
6038
- phase: "ralplan",
6039
- active: true,
6040
- session_id: "sess-autopilot-ralplan-artifact",
6041
- thread_id: "thread-autopilot-ralplan-artifact",
6042
- },
6043
- ],
6553
+ skill: "deep-interview",
6554
+ phase: "planning",
6555
+ session_id: "sess-di-var-redirect",
6556
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-var-redirect" }],
6044
6557
  });
6045
- await writeJson(join(sessionDir, "autopilot-state.json"), {
6558
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
6046
6559
  active: true,
6047
- mode: "autopilot",
6048
- current_phase: "ralplan",
6049
- session_id: "sess-autopilot-ralplan-artifact",
6050
- thread_id: "thread-autopilot-ralplan-artifact",
6560
+ mode: "deep-interview",
6561
+ current_phase: "intent-first",
6562
+ session_id: "sess-di-var-redirect",
6051
6563
  });
6052
6564
 
6053
- const allowedPlanWrite = await dispatchCodexNativeHook(
6565
+ const allowedVarRedirect = await dispatchCodexNativeHook(
6054
6566
  {
6055
6567
  hook_event_name: "PreToolUse",
6056
6568
  cwd,
6057
- session_id: "sess-autopilot-ralplan-artifact",
6058
- thread_id: "thread-autopilot-ralplan-artifact",
6059
- tool_name: "Write",
6060
- tool_use_id: "tool-autopilot-ralplan-plan-write",
6569
+ session_id: "sess-di-var-redirect",
6570
+ tool_name: "Bash",
6571
+ tool_use_id: "tool-di-var-redirect-allow",
6572
+ tool_input: { command: 'SNAP=".omx/context/example.md"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
6573
+ },
6574
+ { cwd },
6575
+ );
6576
+ assert.equal(allowedVarRedirect.outputJson, null);
6577
+
6578
+ const blockedVarRedirect = await dispatchCodexNativeHook(
6579
+ {
6580
+ hook_event_name: "PreToolUse",
6581
+ cwd,
6582
+ session_id: "sess-di-var-redirect",
6583
+ tool_name: "Bash",
6584
+ tool_use_id: "tool-di-var-redirect-block",
6585
+ tool_input: { command: 'SNAP="src/leak.ts"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
6586
+ },
6587
+ { cwd },
6588
+ );
6589
+ assert.equal((blockedVarRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6590
+
6591
+ const blockedUnresolvedVarRedirect = await dispatchCodexNativeHook(
6592
+ {
6593
+ hook_event_name: "PreToolUse",
6594
+ cwd,
6595
+ session_id: "sess-di-var-redirect",
6596
+ tool_name: "Bash",
6597
+ tool_use_id: "tool-di-var-redirect-unresolved",
6598
+ tool_input: { command: 'cat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
6599
+ },
6600
+ { cwd },
6601
+ );
6602
+ assert.equal((blockedUnresolvedVarRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6603
+ } finally {
6604
+ await rm(cwd, { recursive: true, force: true });
6605
+ }
6606
+ });
6607
+
6608
+ it("allows deep-interview apply_patch artifact writes from freeform patch text while blocking outside paths", async () => {
6609
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-apply-patch-"));
6610
+ try {
6611
+ const stateDir = join(cwd, ".omx", "state");
6612
+ const sessionDir = join(stateDir, "sessions", "sess-di-apply-patch");
6613
+ await mkdir(sessionDir, { recursive: true });
6614
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-apply-patch", cwd });
6615
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6616
+ version: 1,
6617
+ active: true,
6618
+ skill: "deep-interview",
6619
+ phase: "planning",
6620
+ session_id: "sess-di-apply-patch",
6621
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-apply-patch" }],
6622
+ });
6623
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
6624
+ active: true,
6625
+ mode: "deep-interview",
6626
+ current_phase: "intent-first",
6627
+ session_id: "sess-di-apply-patch",
6628
+ });
6629
+
6630
+ const allowedAddFile = await dispatchCodexNativeHook(
6631
+ {
6632
+ hook_event_name: "PreToolUse",
6633
+ cwd,
6634
+ session_id: "sess-di-apply-patch",
6635
+ tool_name: "apply_patch",
6636
+ tool_use_id: "tool-di-apply-patch-add",
6637
+ tool_input: {
6638
+ input: "*** Begin Patch\n*** Add File: .omx/context/findings.md\n+# Findings\n*** End Patch\n",
6639
+ },
6640
+ },
6641
+ { cwd },
6642
+ );
6643
+ assert.equal(allowedAddFile.outputJson, null);
6644
+
6645
+ const allowedUpdateFile = await dispatchCodexNativeHook(
6646
+ {
6647
+ hook_event_name: "PreToolUse",
6648
+ cwd,
6649
+ session_id: "sess-di-apply-patch",
6650
+ tool_name: "ApplyPatch",
6651
+ tool_use_id: "tool-di-apply-patch-update",
6652
+ tool_input: {
6653
+ input: "*** Begin Patch\n*** Update File: .omx/specs/deep-interview-demo.md\n@@\n-old\n+new\n*** End Patch\n",
6654
+ },
6655
+ },
6656
+ { cwd },
6657
+ );
6658
+ assert.equal(allowedUpdateFile.outputJson, null);
6659
+
6660
+ const allowedStateWrite = await dispatchCodexNativeHook(
6661
+ {
6662
+ hook_event_name: "PreToolUse",
6663
+ cwd,
6664
+ session_id: "sess-di-apply-patch",
6665
+ tool_name: "apply_patch",
6666
+ tool_use_id: "tool-di-apply-patch-state",
6667
+ tool_input: {
6668
+ input: "*** Begin Patch\n*** Add File: .omx/state/deep-interview-notes.json\n+{}\n*** End Patch\n",
6669
+ },
6670
+ },
6671
+ { cwd },
6672
+ );
6673
+ assert.equal(allowedStateWrite.outputJson, null);
6674
+
6675
+ const blockedOutsidePath = await dispatchCodexNativeHook(
6676
+ {
6677
+ hook_event_name: "PreToolUse",
6678
+ cwd,
6679
+ session_id: "sess-di-apply-patch",
6680
+ tool_name: "apply_patch",
6681
+ tool_use_id: "tool-di-apply-patch-outside",
6682
+ tool_input: {
6683
+ input: "*** Begin Patch\n*** Add File: src/implementation.ts\n+export const x = 1;\n*** End Patch\n",
6684
+ },
6685
+ },
6686
+ { cwd },
6687
+ );
6688
+ assert.equal((blockedOutsidePath.outputJson as { decision?: string } | null)?.decision, "block");
6689
+ assert.match(String((blockedOutsidePath.outputJson as { reason?: string } | null)?.reason ?? ""), /Deep-interview is active/);
6690
+
6691
+ const blockedMixedPaths = await dispatchCodexNativeHook(
6692
+ {
6693
+ hook_event_name: "PreToolUse",
6694
+ cwd,
6695
+ session_id: "sess-di-apply-patch",
6696
+ tool_name: "apply_patch",
6697
+ tool_use_id: "tool-di-apply-patch-mixed",
6698
+ tool_input: {
6699
+ input: "*** Begin Patch\n*** Add File: .omx/context/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
6700
+ },
6701
+ },
6702
+ { cwd },
6703
+ );
6704
+ assert.equal((blockedMixedPaths.outputJson as { decision?: string } | null)?.decision, "block");
6705
+
6706
+ const blockedUnparseablePatch = await dispatchCodexNativeHook(
6707
+ {
6708
+ hook_event_name: "PreToolUse",
6709
+ cwd,
6710
+ session_id: "sess-di-apply-patch",
6711
+ tool_name: "apply_patch",
6712
+ tool_use_id: "tool-di-apply-patch-empty",
6713
+ tool_input: { input: "not a recognizable patch" },
6714
+ },
6715
+ { cwd },
6716
+ );
6717
+ assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
6718
+ } finally {
6719
+ await rm(cwd, { recursive: true, force: true });
6720
+ }
6721
+ });
6722
+
6723
+ it("allows Ralplan read-only inspection and planning artifact writes while blocking implementation targets", async () => {
6724
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-guard-"));
6725
+ try {
6726
+ const stateDir = join(cwd, ".omx", "state");
6727
+ const sessionId = "sess-ralplan-guard";
6728
+ const sessionDir = join(stateDir, "sessions", sessionId);
6729
+ await mkdir(sessionDir, { recursive: true });
6730
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
6731
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6732
+ version: 1,
6733
+ active: true,
6734
+ skill: "ralplan",
6735
+ phase: "planning",
6736
+ session_id: sessionId,
6737
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
6738
+ });
6739
+ await writeJson(join(sessionDir, "ralplan-state.json"), {
6740
+ active: true,
6741
+ mode: "ralplan",
6742
+ current_phase: "planning",
6743
+ session_id: sessionId,
6744
+ });
6745
+
6746
+ const preToolUse = async (tool_name: string, tool_use_id: string, tool_input: Record<string, unknown>) => dispatchCodexNativeHook(
6747
+ {
6748
+ hook_event_name: "PreToolUse",
6749
+ cwd,
6750
+ session_id: sessionId,
6751
+ tool_name,
6752
+ tool_use_id,
6753
+ tool_input,
6754
+ },
6755
+ { cwd },
6756
+ );
6757
+
6758
+ const readOnlyCommands = [
6759
+ "git status --short",
6760
+ "ls -1 .omx/plans",
6761
+ "find .omx/plans -type f -name '*.md'",
6762
+ "grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
6763
+ "rg \"Scholastic\" doc tests .omx/plans .omx/context",
6764
+ "sed -n '1,80p' .omx/plans/demo.md",
6765
+ "cat .omx/plans/demo.md",
6766
+ "git status --short; ls -1 .omx/plans; grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
6767
+ ];
6768
+ for (const [index, command] of readOnlyCommands.entries()) {
6769
+ const result = await preToolUse("Bash", `tool-ralplan-read-${index}`, { command });
6770
+ assert.equal(result.outputJson, null, `read-only command should be allowed: ${command}`);
6771
+ }
6772
+
6773
+ for (const path of [
6774
+ ".omx/context/findings.md",
6775
+ ".omx/plans/issue-2863.md",
6776
+ ".omx/specs/issue-2863.md",
6777
+ ".omx/state/required-planning-state.json",
6778
+ ]) {
6779
+ const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
6780
+ assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
6781
+ }
6782
+
6783
+ const allowedPatchAdd = await preToolUse("apply_patch", "tool-ralplan-patch-add", {
6784
+ input: "*** Begin Patch\n*** Add File: .omx/plans/issue-2863.md\n+# Plan\n*** End Patch\n",
6785
+ });
6786
+ assert.equal(allowedPatchAdd.outputJson, null);
6787
+
6788
+ const allowedPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-patch-update", {
6789
+ input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
6790
+ });
6791
+ assert.equal(allowedPatchUpdate.outputJson, null);
6792
+
6793
+ const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
6794
+ command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
6795
+ });
6796
+ assert.equal(allowedRedirect.outputJson, null);
6797
+
6798
+ const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
6799
+ command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
6800
+ });
6801
+ assert.equal(allowedTee.outputJson, null);
6802
+
6803
+ const blockedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-block", {
6804
+ command: "printf 'bad' > src/implementation.ts",
6805
+ });
6806
+ assert.equal((blockedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6807
+ const redirectReason = String((blockedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
6808
+ assert.match(redirectReason, /Bash redirect write/);
6809
+ assert.match(redirectReason, /src\/implementation\.ts/);
6810
+
6811
+ const blockedEdit = await preToolUse("Edit", "tool-ralplan-edit-block", {
6812
+ file_path: "src/implementation.ts",
6813
+ old_string: "a",
6814
+ new_string: "b",
6815
+ });
6816
+ assert.equal((blockedEdit.outputJson as { decision?: string } | null)?.decision, "block");
6817
+ const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
6818
+ assert.match(editReason, /Edit path/);
6819
+ assert.match(editReason, /src\/implementation\.ts/);
6820
+
6821
+ const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
6822
+ input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
6823
+ });
6824
+ assert.equal((blockedMixedPatch.outputJson as { decision?: string } | null)?.decision, "block");
6825
+ const mixedReason = String((blockedMixedPatch.outputJson as { reason?: string } | null)?.reason ?? "");
6826
+ assert.match(mixedReason, /apply_patch target/);
6827
+ assert.match(mixedReason, /src\/leak\.ts/);
6828
+
6829
+ const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
6830
+ input: "not a recognizable patch",
6831
+ });
6832
+ assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
6833
+ assert.match(String((blockedUnparseablePatch.outputJson as { reason?: string } | null)?.reason ?? ""), /apply_patch target extraction failed/);
6834
+
6835
+ const blockedUnresolvedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-unresolved", {
6836
+ command: "cat > \"$PLAN_PATH\" <<'EOF'\ncontent\nEOF",
6837
+ });
6838
+ assert.equal((blockedUnresolvedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
6839
+ const unresolvedReason = String((blockedUnresolvedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
6840
+ assert.match(unresolvedReason, /unresolved Bash write target/);
6841
+ assert.match(unresolvedReason, /\$PLAN_PATH/);
6842
+
6843
+ const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
6844
+ file_path: ".omx/plans/../../src/leak.ts",
6845
+ content: "bad",
6846
+ });
6847
+ assert.equal((blockedTraversal.outputJson as { decision?: string } | null)?.decision, "block");
6848
+ assert.match(String((blockedTraversal.outputJson as { reason?: string } | null)?.reason ?? ""), /\.omx\/plans\/\.\.\/\.\.\/src\/leak\.ts/);
6849
+
6850
+ const blockedAbsolute = await preToolUse("Write", "tool-ralplan-absolute-block", {
6851
+ file_path: "/tmp/leak.ts",
6852
+ content: "bad",
6853
+ });
6854
+ assert.equal((blockedAbsolute.outputJson as { decision?: string } | null)?.decision, "block");
6855
+ assert.match(String((blockedAbsolute.outputJson as { reason?: string } | null)?.reason ?? ""), /\/tmp\/leak\.ts/);
6856
+ } finally {
6857
+ await rm(cwd, { recursive: true, force: true });
6858
+ }
6859
+ });
6860
+
6861
+ it("allows Autopilot ralplan planning artifacts while blocking implementation writes", async () => {
6862
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-ralplan-artifact-"));
6863
+ try {
6864
+ const stateDir = join(cwd, ".omx", "state");
6865
+ const sessionDir = join(stateDir, "sessions", "sess-autopilot-ralplan-artifact");
6866
+ await mkdir(sessionDir, { recursive: true });
6867
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-ralplan-artifact", cwd });
6868
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6869
+ version: 1,
6870
+ active: true,
6871
+ skill: "autopilot",
6872
+ phase: "ralplan",
6873
+ session_id: "sess-autopilot-ralplan-artifact",
6874
+ thread_id: "thread-autopilot-ralplan-artifact",
6875
+ active_skills: [
6876
+ {
6877
+ skill: "autopilot",
6878
+ phase: "ralplan",
6879
+ active: true,
6880
+ session_id: "sess-autopilot-ralplan-artifact",
6881
+ thread_id: "thread-autopilot-ralplan-artifact",
6882
+ },
6883
+ ],
6884
+ });
6885
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
6886
+ active: true,
6887
+ mode: "autopilot",
6888
+ current_phase: "ralplan",
6889
+ session_id: "sess-autopilot-ralplan-artifact",
6890
+ thread_id: "thread-autopilot-ralplan-artifact",
6891
+ });
6892
+
6893
+ const allowedPlanWrite = await dispatchCodexNativeHook(
6894
+ {
6895
+ hook_event_name: "PreToolUse",
6896
+ cwd,
6897
+ session_id: "sess-autopilot-ralplan-artifact",
6898
+ thread_id: "thread-autopilot-ralplan-artifact",
6899
+ tool_name: "Write",
6900
+ tool_use_id: "tool-autopilot-ralplan-plan-write",
6061
6901
  tool_input: { file_path: ".omx/plans/prd-omx-y7a.md", content: "# Plan" },
6062
6902
  },
6063
6903
  { cwd },
@@ -6098,6 +6938,263 @@ exit 0
6098
6938
  }
6099
6939
  });
6100
6940
 
6941
+ it("allows Ralplan Beads tracker metadata writes during planning", async () => {
6942
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-beads-"));
6943
+ try {
6944
+ const stateDir = join(cwd, ".omx", "state");
6945
+ const sessionDir = join(stateDir, "sessions", "sess-ralplan-beads");
6946
+ await mkdir(sessionDir, { recursive: true });
6947
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-ralplan-beads", cwd });
6948
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
6949
+ version: 1,
6950
+ active: true,
6951
+ skill: "ralplan",
6952
+ phase: "planning",
6953
+ session_id: "sess-ralplan-beads",
6954
+ thread_id: "thread-ralplan-beads",
6955
+ active_skills: [
6956
+ {
6957
+ skill: "ralplan",
6958
+ phase: "planning",
6959
+ active: true,
6960
+ session_id: "sess-ralplan-beads",
6961
+ thread_id: "thread-ralplan-beads",
6962
+ },
6963
+ ],
6964
+ });
6965
+ await writeJson(join(sessionDir, "ralplan-state.json"), {
6966
+ active: true,
6967
+ mode: "ralplan",
6968
+ current_phase: "planning",
6969
+ session_id: "sess-ralplan-beads",
6970
+ thread_id: "thread-ralplan-beads",
6971
+ });
6972
+
6973
+ const allowedWrite = await dispatchCodexNativeHook(
6974
+ {
6975
+ hook_event_name: "PreToolUse",
6976
+ cwd,
6977
+ session_id: "sess-ralplan-beads",
6978
+ thread_id: "thread-ralplan-beads",
6979
+ tool_name: "Write",
6980
+ tool_use_id: "tool-ralplan-beads-write",
6981
+ tool_input: { file_path: ".beads/issues.db", content: "metadata" },
6982
+ },
6983
+ { cwd },
6984
+ );
6985
+ assert.equal(allowedWrite.outputJson, null);
6986
+
6987
+ const allowedEdit = await dispatchCodexNativeHook(
6988
+ {
6989
+ hook_event_name: "PreToolUse",
6990
+ cwd,
6991
+ session_id: "sess-ralplan-beads",
6992
+ thread_id: "thread-ralplan-beads",
6993
+ tool_name: "Edit",
6994
+ tool_use_id: "tool-ralplan-beads-edit",
6995
+ tool_input: { file_path: ".beads/tasks/issue.json", old_string: "old", new_string: "new" },
6996
+ },
6997
+ { cwd },
6998
+ );
6999
+ assert.equal(allowedEdit.outputJson, null);
7000
+
7001
+ const allowedPatch = await dispatchCodexNativeHook(
7002
+ {
7003
+ hook_event_name: "PreToolUse",
7004
+ cwd,
7005
+ session_id: "sess-ralplan-beads",
7006
+ thread_id: "thread-ralplan-beads",
7007
+ tool_name: "apply_patch",
7008
+ tool_use_id: "tool-ralplan-beads-apply-patch",
7009
+ tool_input: {
7010
+ input: "*** Begin Patch\n*** Add File: .beads/tasks/issue.json\n+{}\n*** End Patch\n",
7011
+ },
7012
+ },
7013
+ { cwd },
7014
+ );
7015
+ assert.equal(allowedPatch.outputJson, null);
7016
+
7017
+ const allowedCommands = [
7018
+ "bd --db .beads/issues.db create 'Issue title'",
7019
+ "bd --db .beads/issues.db update OMX-1 --title 'Issue title'",
7020
+ "bd --db .beads/issues.db edit OMX-1 --title 'Issue title'",
7021
+ "bd --db .beads/issues.db comments add OMX-1 'evidence'",
7022
+ "bd --db .beads/issues.db close OMX-1",
7023
+ "bd --db .beads/issues.db reopen OMX-1",
7024
+ "bd --db .beads/issues.db status OMX-1",
7025
+ "bd --db .beads/issues.db dep add OMX-1 OMX-2",
7026
+ ];
7027
+
7028
+ for (const [index, command] of allowedCommands.entries()) {
7029
+ const result = await dispatchCodexNativeHook(
7030
+ {
7031
+ hook_event_name: "PreToolUse",
7032
+ cwd,
7033
+ session_id: "sess-ralplan-beads",
7034
+ thread_id: "thread-ralplan-beads",
7035
+ tool_name: "Bash",
7036
+ tool_use_id: `tool-ralplan-beads-bd-${index}`,
7037
+ tool_input: { command },
7038
+ },
7039
+ { cwd },
7040
+ );
7041
+ assert.equal(result.outputJson, null, `expected Beads metadata command to be allowed: ${command}`);
7042
+ }
7043
+ } finally {
7044
+ await rm(cwd, { recursive: true, force: true });
7045
+ }
7046
+ });
7047
+
7048
+ it("blocks unsafe Beads tracker targets and mixed implementation writes during Autopilot ralplan planning", async () => {
7049
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-beads-block-"));
7050
+ try {
7051
+ const stateDir = join(cwd, ".omx", "state");
7052
+ const sessionDir = join(stateDir, "sessions", "sess-autopilot-beads-block");
7053
+ await mkdir(sessionDir, { recursive: true });
7054
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-beads-block", cwd });
7055
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
7056
+ version: 1,
7057
+ active: true,
7058
+ skill: "autopilot",
7059
+ phase: "ralplan",
7060
+ session_id: "sess-autopilot-beads-block",
7061
+ thread_id: "thread-autopilot-beads-block",
7062
+ active_skills: [
7063
+ {
7064
+ skill: "autopilot",
7065
+ phase: "ralplan",
7066
+ active: true,
7067
+ session_id: "sess-autopilot-beads-block",
7068
+ thread_id: "thread-autopilot-beads-block",
7069
+ },
7070
+ ],
7071
+ });
7072
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
7073
+ active: true,
7074
+ mode: "autopilot",
7075
+ current_phase: "ralplan",
7076
+ session_id: "sess-autopilot-beads-block",
7077
+ thread_id: "thread-autopilot-beads-block",
7078
+ });
7079
+
7080
+ const blockedCommands = [
7081
+ "bd --db /tmp/outside.db create 'Issue title'",
7082
+ "bd --db ../outside.db create 'Issue title'",
7083
+ "bd --db .beads create 'Issue title'",
7084
+ "bd --db $DB create 'Issue title'",
7085
+ "DB=.beads/issues.db bd --db $DB create 'Issue title'",
7086
+ "env bd --db /tmp/outside.db create 'Issue title'",
7087
+ "command bd --db /tmp/outside.db create 'Issue title'",
7088
+ "bd --db .beads/issues.db export",
7089
+ "bd --db .beads/issues.db create 'Issue title' > src/leak.ts",
7090
+ "bd --db .beads/issues.db create 'Issue title'; cat > src/leak.ts",
7091
+ ];
7092
+
7093
+ for (const [index, command] of blockedCommands.entries()) {
7094
+ const result = await dispatchCodexNativeHook(
7095
+ {
7096
+ hook_event_name: "PreToolUse",
7097
+ cwd,
7098
+ session_id: "sess-autopilot-beads-block",
7099
+ thread_id: "thread-autopilot-beads-block",
7100
+ tool_name: "Bash",
7101
+ tool_use_id: `tool-autopilot-beads-block-${index}`,
7102
+ tool_input: { command },
7103
+ },
7104
+ { cwd },
7105
+ );
7106
+ assert.equal(
7107
+ (result.outputJson as { decision?: string } | null)?.decision,
7108
+ "block",
7109
+ `expected unsafe Beads command to be blocked: ${command}`,
7110
+ );
7111
+ }
7112
+
7113
+ const mentionWithImplementationWrite = await dispatchCodexNativeHook(
7114
+ {
7115
+ hook_event_name: "PreToolUse",
7116
+ cwd,
7117
+ session_id: "sess-autopilot-beads-block",
7118
+ thread_id: "thread-autopilot-beads-block",
7119
+ tool_name: "Bash",
7120
+ tool_use_id: "tool-autopilot-beads-mention-write",
7121
+ tool_input: { command: "echo bd --db .beads/issues.db create > src/leak.ts" },
7122
+ },
7123
+ { cwd },
7124
+ );
7125
+ assert.equal((mentionWithImplementationWrite.outputJson as { decision?: string } | null)?.decision, "block");
7126
+ assert.match(String((mentionWithImplementationWrite.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
7127
+
7128
+ const blockedImplementationEdit = await dispatchCodexNativeHook(
7129
+ {
7130
+ hook_event_name: "PreToolUse",
7131
+ cwd,
7132
+ session_id: "sess-autopilot-beads-block",
7133
+ thread_id: "thread-autopilot-beads-block",
7134
+ tool_name: "Edit",
7135
+ tool_use_id: "tool-autopilot-beads-src-edit",
7136
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
7137
+ },
7138
+ { cwd },
7139
+ );
7140
+ assert.equal((blockedImplementationEdit.outputJson as { decision?: string } | null)?.decision, "block");
7141
+ } finally {
7142
+ await rm(cwd, { recursive: true, force: true });
7143
+ }
7144
+ });
7145
+
7146
+ it("allows Autopilot rework implementation writes while ralplan remains guarded", async () => {
7147
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-rework-"));
7148
+ try {
7149
+ const stateDir = join(cwd, ".omx", "state");
7150
+ const sessionDir = join(stateDir, "sessions", "sess-autopilot-rework");
7151
+ await mkdir(sessionDir, { recursive: true });
7152
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-rework", cwd });
7153
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
7154
+ version: 1,
7155
+ active: true,
7156
+ skill: "autopilot",
7157
+ phase: "rework",
7158
+ session_id: "sess-autopilot-rework",
7159
+ thread_id: "thread-autopilot-rework",
7160
+ active_skills: [
7161
+ {
7162
+ skill: "autopilot",
7163
+ phase: "rework",
7164
+ active: true,
7165
+ session_id: "sess-autopilot-rework",
7166
+ thread_id: "thread-autopilot-rework",
7167
+ },
7168
+ ],
7169
+ });
7170
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
7171
+ active: true,
7172
+ mode: "autopilot",
7173
+ current_phase: "rework",
7174
+ review_cycle: 2,
7175
+ review_verdict: { clean: false, recommendation: "REQUEST_CHANGES", findings: ["src/implementation.ts"] },
7176
+ session_id: "sess-autopilot-rework",
7177
+ thread_id: "thread-autopilot-rework",
7178
+ });
7179
+
7180
+ const allowedImplementationEdit = await dispatchCodexNativeHook(
7181
+ {
7182
+ hook_event_name: "PreToolUse",
7183
+ cwd,
7184
+ session_id: "sess-autopilot-rework",
7185
+ thread_id: "thread-autopilot-rework",
7186
+ tool_name: "Edit",
7187
+ tool_use_id: "tool-autopilot-rework-src-edit",
7188
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
7189
+ },
7190
+ { cwd },
7191
+ );
7192
+ assert.equal(allowedImplementationEdit.outputJson, null);
7193
+ } finally {
7194
+ await rm(cwd, { recursive: true, force: true });
7195
+ }
7196
+ });
7197
+
6101
7198
  it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
6102
7199
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
6103
7200
  try {
@@ -6267,6 +7364,207 @@ exit 0
6267
7364
  }
6268
7365
  });
6269
7366
 
7367
+ it("records native subagent capacity exhaustion from spawn_agent PostToolUse output", async () => {
7368
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-record-"));
7369
+ try {
7370
+ const result = await dispatchCodexNativeHook(
7371
+ {
7372
+ hook_event_name: "PostToolUse",
7373
+ cwd,
7374
+ session_id: "sess-subagent-capacity-record",
7375
+ thread_id: "thread-subagent-capacity-record",
7376
+ turn_id: "turn-subagent-capacity-record",
7377
+ tool_name: "multi_agent_v1.spawn_agent",
7378
+ tool_response: {
7379
+ error: "collab spawn failed: agent thread limit reached",
7380
+ },
7381
+ },
7382
+ { cwd },
7383
+ );
7384
+
7385
+ assert.equal(result.omxEventName, "post-tool-use");
7386
+ assert.equal(result.outputJson, null);
7387
+
7388
+ const blocker = JSON.parse(
7389
+ await readFile(join(cwd, ".omx", "state", "native-subagent-capacity-blocker.json"), "utf-8"),
7390
+ ) as Record<string, unknown>;
7391
+ assert.equal(blocker.reason, "agent_thread_limit_reached");
7392
+ assert.equal(blocker.session_id, "sess-subagent-capacity-record");
7393
+ assert.equal(blocker.thread_id, "thread-subagent-capacity-record");
7394
+ assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
7395
+ assert.match(String(blocker.error_summary), /agent thread limit reached/);
7396
+ assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
7397
+ } finally {
7398
+ await rm(cwd, { recursive: true, force: true });
7399
+ }
7400
+ });
7401
+
7402
+ it("blocks close_agent cleanup after recent native subagent capacity exhaustion", async () => {
7403
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-block-"));
7404
+ try {
7405
+ await dispatchCodexNativeHook(
7406
+ {
7407
+ hook_event_name: "PostToolUse",
7408
+ cwd,
7409
+ session_id: "sess-subagent-capacity-close-block",
7410
+ thread_id: "thread-subagent-capacity-close-block",
7411
+ turn_id: "turn-subagent-capacity-close-block",
7412
+ tool_name: "multi_agent_v1.spawn_agent",
7413
+ tool_response: "collab spawn failed: agent thread limit reached",
7414
+ },
7415
+ { cwd },
7416
+ );
7417
+
7418
+ const result = await dispatchCodexNativeHook(
7419
+ {
7420
+ hook_event_name: "PreToolUse",
7421
+ cwd,
7422
+ session_id: "sess-subagent-capacity-close-block",
7423
+ thread_id: "thread-subagent-capacity-close-block",
7424
+ tool_name: "multi_agent_v1.close_agent",
7425
+ tool_input: { target: "019ecc36-stale" },
7426
+ },
7427
+ { cwd },
7428
+ );
7429
+
7430
+ assert.equal(result.omxEventName, "pre-tool-use");
7431
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
7432
+ assert.match(JSON.stringify(result.outputJson), /agent thread limit reached/);
7433
+ assert.match(JSON.stringify(result.outputJson), /multi_agent_v1\.close_agent/);
7434
+ assert.match(JSON.stringify(result.outputJson), /multi_tool_use\.parallel/);
7435
+ assert.match(JSON.stringify(result.outputJson), /bounded capacity blocker/);
7436
+ } finally {
7437
+ await rm(cwd, { recursive: true, force: true });
7438
+ }
7439
+ });
7440
+
7441
+ it("emits schema-safe PreToolUse CLI stdout for close_agent capacity blocks", async () => {
7442
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-subagent-capacity-close-block-"));
7443
+ try {
7444
+ parseSingleJsonStdout(runNativeHookCli({
7445
+ hook_event_name: "PostToolUse",
7446
+ cwd,
7447
+ session_id: "sess-cli-subagent-capacity-close-block",
7448
+ thread_id: "thread-cli-subagent-capacity-close-block",
7449
+ turn_id: "turn-cli-subagent-capacity-close-block",
7450
+ tool_name: "multi_agent_v1.spawn_agent",
7451
+ tool_response: "collab spawn failed: agent thread limit reached",
7452
+ }, { cwd }));
7453
+
7454
+ const output = parseSingleJsonStdout(runNativeHookCli({
7455
+ hook_event_name: "PreToolUse",
7456
+ cwd,
7457
+ session_id: "sess-cli-subagent-capacity-close-block",
7458
+ thread_id: "thread-cli-subagent-capacity-close-block",
7459
+ tool_name: "multi_agent_v1.close_agent",
7460
+ tool_input: { target: "019ecc36-stale" },
7461
+ }, { cwd }));
7462
+
7463
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
7464
+ assert.match(String(output.systemMessage ?? ""), /agent thread limit reached/);
7465
+ assert.match(String(output.systemMessage ?? ""), /Do not call multi_agent_v1\.close_agent/);
7466
+ assert.equal(output.decision, undefined);
7467
+ assert.equal(output.hookSpecificOutput, undefined);
7468
+ } finally {
7469
+ await rm(cwd, { recursive: true, force: true });
7470
+ }
7471
+ });
7472
+
7473
+ it("blocks parallel close_agent cleanup after recent native subagent capacity exhaustion", async () => {
7474
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-parallel-close-block-"));
7475
+ try {
7476
+ await dispatchCodexNativeHook(
7477
+ {
7478
+ hook_event_name: "PostToolUse",
7479
+ cwd,
7480
+ session_id: "sess-subagent-capacity-parallel-close-block",
7481
+ thread_id: "thread-subagent-capacity-parallel-close-block",
7482
+ tool_name: "multi_agent_v1.spawn_agent",
7483
+ tool_response: "agent thread limit reached",
7484
+ },
7485
+ { cwd },
7486
+ );
7487
+
7488
+ const result = await dispatchCodexNativeHook(
7489
+ {
7490
+ hook_event_name: "PreToolUse",
7491
+ cwd,
7492
+ session_id: "sess-subagent-capacity-parallel-close-block",
7493
+ thread_id: "thread-subagent-capacity-parallel-close-block",
7494
+ tool_name: "multi_tool_use.parallel",
7495
+ tool_input: {
7496
+ tool_uses: [
7497
+ { recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-1" } },
7498
+ { recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-2" } },
7499
+ ],
7500
+ },
7501
+ },
7502
+ { cwd },
7503
+ );
7504
+
7505
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
7506
+ assert.match(JSON.stringify(result.outputJson), /Do not call multi_agent_v1\.close_agent/);
7507
+ assert.match(JSON.stringify(result.outputJson), /do not batch close_agent through multi_tool_use\.parallel/);
7508
+ } finally {
7509
+ await rm(cwd, { recursive: true, force: true });
7510
+ }
7511
+ });
7512
+
7513
+ it("does not block close_agent without a recent native capacity blocker", async () => {
7514
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-no-blocker-"));
7515
+ try {
7516
+ const result = await dispatchCodexNativeHook(
7517
+ {
7518
+ hook_event_name: "PreToolUse",
7519
+ cwd,
7520
+ session_id: "sess-subagent-capacity-close-no-blocker",
7521
+ thread_id: "thread-subagent-capacity-close-no-blocker",
7522
+ tool_name: "multi_agent_v1.close_agent",
7523
+ tool_input: { target: "completed-agent" },
7524
+ },
7525
+ { cwd },
7526
+ );
7527
+
7528
+ assert.equal(result.omxEventName, "pre-tool-use");
7529
+ assert.equal(result.outputJson, null);
7530
+ } finally {
7531
+ await rm(cwd, { recursive: true, force: true });
7532
+ }
7533
+ });
7534
+
7535
+ it("does not block close_agent when the native capacity blocker is expired", async () => {
7536
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-expired-"));
7537
+ try {
7538
+ const stateDir = join(cwd, ".omx", "state");
7539
+ await mkdir(stateDir, { recursive: true });
7540
+ await writeJson(join(stateDir, "native-subagent-capacity-blocker.json"), {
7541
+ schema_version: 1,
7542
+ reason: "agent_thread_limit_reached",
7543
+ session_id: "sess-subagent-capacity-close-expired",
7544
+ error_summary: "agent thread limit reached",
7545
+ observed_at: "2026-06-20T00:00:00.000Z",
7546
+ expires_at: "2026-06-20T00:30:00.000Z",
7547
+ cwd,
7548
+ });
7549
+
7550
+ const result = await dispatchCodexNativeHook(
7551
+ {
7552
+ hook_event_name: "PreToolUse",
7553
+ cwd,
7554
+ session_id: "sess-subagent-capacity-close-expired",
7555
+ thread_id: "thread-subagent-capacity-close-expired",
7556
+ tool_name: "multi_agent_v1.close_agent",
7557
+ tool_input: { target: "completed-agent" },
7558
+ },
7559
+ { cwd },
7560
+ );
7561
+
7562
+ assert.equal(result.outputJson, null);
7563
+ } finally {
7564
+ await rm(cwd, { recursive: true, force: true });
7565
+ }
7566
+ });
7567
+
6270
7568
  it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
6271
7569
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
6272
7570
  try {