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
@@ -13,6 +13,7 @@ import {
13
13
  readSkillActiveState,
14
14
  readVisibleSkillActiveStateForStateDir,
15
15
  type SkillActiveStateLike,
16
+ type SkillActiveEntry,
16
17
  } from "../state/skill-active.js";
17
18
  import {
18
19
  isTrustedSubagentThread,
@@ -40,7 +41,7 @@ import {
40
41
  } from "../team/state.js";
41
42
  import { omxNotepadPath, resolveProjectMemoryPath } from "../utils/paths.js";
42
43
  import { findGitLayout } from "../utils/git-layout.js";
43
- import { getBaseStateDir, getStateFilePath, getStatePath } from "../mcp/state-paths.js";
44
+ import { getBaseStateDir, getStateFilePath, getStatePath, getAuthoritativeActiveStatePaths } from "../mcp/state-paths.js";
44
45
  import {
45
46
  detectKeywords,
46
47
  detectPrimaryKeyword,
@@ -87,6 +88,9 @@ import { readAutoresearchCompletionStatus, readAutoresearchModeStateForActiveDec
87
88
  import { normalizeAutopilotPhase } from "../autopilot/fsm.js";
88
89
  import { readRunState } from "../runtime/run-state.js";
89
90
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
91
+ import {
92
+ buildCodexGoalTerminalCleanupNotice,
93
+ } from "../goal-workflows/codex-goal-snapshot.js";
90
94
  import { getRunContinuationSnapshot, shouldContinueRun } from "../runtime/run-loop.js";
91
95
  import {
92
96
  parseUltragoalSteeringDirective,
@@ -147,6 +151,8 @@ const SKILL_STOP_BLOCKERS = new Set(["ralplan"]);
147
151
  const TEAM_STOP_BLOCKING_TASK_STATUSES = new Set(["pending", "in_progress", "blocked"]);
148
152
  const TEAM_WORKER_TERMINAL_RUN_STATES = new Set(["done", "complete", "completed", "failed", "stopped", "cancelled"]);
149
153
  const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
154
+ const NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE = "native-subagent-capacity-blocker.json";
155
+ const NATIVE_SUBAGENT_CAPACITY_BLOCKER_TTL_MS = 30 * 60_000;
150
156
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS = 8;
151
157
  const RALPH_ORPHANED_STARTING_STALE_MS = 15 * 60_000;
152
158
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_IDLE_MS = 10 * 60_000;
@@ -549,6 +555,23 @@ function readHookEventName(payload: CodexHookPayload): CodexHookEventName | null
549
555
  return null;
550
556
  }
551
557
 
558
+ function sanitizeCodexHookOutput(
559
+ hookEventName: CodexHookEventName | null,
560
+ output: Record<string, unknown> | null,
561
+ ): Record<string, unknown> | null {
562
+ if (!output || hookEventName !== "PreToolUse") return output;
563
+ const systemMessage = safeString(output.systemMessage).trim();
564
+ if (systemMessage) return { systemMessage };
565
+
566
+ const reason = safeString(output.reason).trim();
567
+ const hookSpecificOutput = output.hookSpecificOutput;
568
+ const additionalContext = hookSpecificOutput && typeof hookSpecificOutput === "object"
569
+ ? safeString((hookSpecificOutput as { additionalContext?: unknown }).additionalContext).trim()
570
+ : "";
571
+ const derivedSystemMessage = [reason, additionalContext].filter(Boolean).join("\n\n");
572
+ return derivedSystemMessage ? { systemMessage: derivedSystemMessage } : {};
573
+ }
574
+
552
575
  export function mapCodexHookEventToOmxEvent(
553
576
  hookEventName: CodexHookEventName | null,
554
577
  ): string | null {
@@ -2227,6 +2250,76 @@ function isStopExempt(payload: CodexHookPayload): boolean {
2227
2250
  );
2228
2251
  }
2229
2252
 
2253
+ async function readModeStateWithStopSource(
2254
+ mode: "autopilot" | "ultrawork" | "ultraqa",
2255
+ cwd: string,
2256
+ sessionId?: string,
2257
+ ): Promise<{ state: Record<string, unknown>; path: string } | null> {
2258
+ const paths = await getAuthoritativeActiveStatePaths(mode, cwd, sessionId?.trim() || undefined).catch(() => [] as string[]);
2259
+ const path = paths[0];
2260
+ if (!path) return null;
2261
+ const state = await readJsonIfExists(path);
2262
+ return state ? { state, path } : null;
2263
+ }
2264
+ async function readRawSkillActiveState(path: string): Promise<SkillActiveStateLike | null> {
2265
+ try {
2266
+ const parsed = JSON.parse(await readFile(path, "utf-8"));
2267
+ return parsed && typeof parsed === "object" ? parsed as SkillActiveStateLike : null;
2268
+ } catch {
2269
+ return null;
2270
+ }
2271
+ }
2272
+
2273
+
2274
+ function canonicalStopDisagreement(modeState: Record<string, unknown>, canonicalState: SkillActiveStateLike | null, mode: string, sessionId?: string): string {
2275
+ if (!canonicalState) return "canonical_state_missing";
2276
+ const normalizedSessionId = safeString(sessionId).trim();
2277
+ const activeEntry = listRawActiveSkillEntries(canonicalState).find((entry) => {
2278
+ if (entry.skill !== mode) return false;
2279
+ const entrySessionId = safeString(entry.session_id ?? canonicalState.session_id).trim();
2280
+ return normalizedSessionId ? entrySessionId === normalizedSessionId || entrySessionId === "" : true;
2281
+ });
2282
+ if (!activeEntry) return "canonical_inactive";
2283
+ if (mode === "autopilot") {
2284
+ const phase = safeString(modeState.current_phase ?? modeState.currentPhase).trim();
2285
+ const canonicalPhase = safeString(activeEntry.phase ?? canonicalState.phase).trim();
2286
+ if (phase && canonicalPhase && normalizeAutopilotPhase(phase) !== normalizeAutopilotPhase(canonicalPhase)) {
2287
+ return `canonical_phase:${canonicalPhase}`;
2288
+ }
2289
+ }
2290
+ return "canonical_agrees";
2291
+ }
2292
+
2293
+ function listRawActiveSkillEntries(state: SkillActiveStateLike | null): SkillActiveEntry[] {
2294
+ if (!state) return [];
2295
+ const entries: SkillActiveEntry[] = [];
2296
+ if (Array.isArray(state.active_skills)) {
2297
+ for (const candidate of state.active_skills) {
2298
+ if (!candidate || typeof candidate !== "object") continue;
2299
+ const raw = candidate as unknown as Record<string, unknown>;
2300
+ const skill = safeString(raw.skill).trim();
2301
+ if (!skill || raw.active === false) continue;
2302
+ entries.push({
2303
+ ...raw,
2304
+ skill,
2305
+ phase: safeString(raw.phase).trim() || undefined,
2306
+ session_id: safeString(raw.session_id).trim() || undefined,
2307
+ thread_id: safeString(raw.thread_id).trim() || undefined,
2308
+ });
2309
+ }
2310
+ }
2311
+ const topLevelSkill = safeString(state.skill).trim();
2312
+ if (state.active === true && topLevelSkill) {
2313
+ entries.push({
2314
+ skill: topLevelSkill,
2315
+ phase: safeString(state.phase).trim() || undefined,
2316
+ session_id: safeString(state.session_id).trim() || undefined,
2317
+ thread_id: safeString(state.thread_id).trim() || undefined,
2318
+ });
2319
+ }
2320
+ return entries;
2321
+ }
2322
+
2230
2323
  async function buildModeBasedStopOutput(
2231
2324
  mode: "autopilot" | "ultrawork" | "ultraqa",
2232
2325
  cwd: string,
@@ -2238,17 +2331,38 @@ async function buildModeBasedStopOutput(
2238
2331
  if (mode === "autopilot" && await readAutopilotDeepInterviewQuestionWaitState(cwd, sessionId)) {
2239
2332
  return null;
2240
2333
  }
2241
- const state = await readModeStateForActiveDecision(mode, sessionId?.trim() || undefined, cwd);
2334
+ const sourcedState = await readModeStateWithStopSource(mode, cwd, sessionId);
2335
+ const state = sourcedState?.state ?? null;
2242
2336
  if (!state || !shouldContinueRun(state)) return null;
2337
+ const rootCanonicalState = await readRawSkillActiveState(getSkillActiveStatePathsForStateDir(getBaseStateDir(cwd)).rootPath);
2338
+ const canonicalDisagreement = rootCanonicalState
2339
+ ? canonicalStopDisagreement(state, rootCanonicalState, mode, sessionId)
2340
+ : "canonical_state_missing";
2341
+ if (canonicalDisagreement === "canonical_inactive") return null;
2243
2342
  const phase = formatPhase(state.current_phase);
2343
+ if (!rootCanonicalState || mode !== "autopilot") {
2344
+ const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
2345
+ ? "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state."
2346
+ : `OMX ${mode} is still active (phase: ${phase}).`;
2347
+ return {
2348
+ decision: "block",
2349
+ reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
2350
+ stopReason: `${mode}_${phase}`,
2351
+ systemMessage,
2352
+ };
2353
+ }
2354
+ const statePath = sourcedState ? formatStopStatePath(cwd, sourcedState.path) : "unknown";
2355
+ const diagnostic = `state: ${statePath}; canonical: ${canonicalDisagreement}`;
2244
2356
  const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
2245
- ? "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state."
2246
- : `OMX ${mode} is still active (phase: ${phase}).`;
2357
+ ? `OMX autopilot is still active (phase: code-review; ${diagnostic}). Run the required $code-review step before completing or clearing Autopilot state.`
2358
+ : `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}).`;
2247
2359
  return {
2248
2360
  decision: "block",
2249
- reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
2361
+ reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
2250
2362
  stopReason: `${mode}_${phase}`,
2251
2363
  systemMessage,
2364
+ statePath,
2365
+ canonicalDisagreement,
2252
2366
  };
2253
2367
  }
2254
2368
 
@@ -2294,6 +2408,116 @@ function reportsBlockedUltragoalCompletedAggregateMicrogoalLoop(goal: Record<str
2294
2408
  && /\b(?:unreconcilable|mismatch|loop|already complete|already completed|blocks?)\b/i.test(evidence);
2295
2409
  }
2296
2410
 
2411
+
2412
+ function sentenceWindowAround(text: string, start: number, end: number): string {
2413
+ const rawBefore = text.slice(Math.max(0, start - 80), start);
2414
+ const rawAfter = text.slice(end, Math.min(text.length, end + 80));
2415
+ const before = rawBefore.slice(Math.max(rawBefore.lastIndexOf("\n"), rawBefore.lastIndexOf("."), rawBefore.lastIndexOf("!"), rawBefore.lastIndexOf("?")) + 1);
2416
+ const sentenceEndOffsets = [rawAfter.indexOf("\n"), rawAfter.indexOf("."), rawAfter.indexOf("!"), rawAfter.indexOf("?")].filter((index) => index >= 0);
2417
+ const after = sentenceEndOffsets.length > 0 ? rawAfter.slice(0, Math.min(...sentenceEndOffsets)) : rawAfter;
2418
+ return `${before}${text.slice(start, end)}${after}`;
2419
+ }
2420
+
2421
+ function isNegatedGoalAttemptWindow(window: string): boolean {
2422
+ return /\b(?:do\s+not|don't|never|must\s+not|should\s+not|cannot|can't|not|no)\b.{0,50}\b(?:start|create|begin|new|another|goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b/i.test(window)
2423
+ || /\b(?:without|instead\s+of)\b.{0,30}\b(?:start|create|begin|new|another)?\b.{0,30}\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b/i.test(window)
2424
+ || /\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b.{0,30}\b(?:is|was|already)\b.{0,30}\b(?:documented|complete|completed|done|unavailable|not\s+needed)\b/i.test(window);
2425
+ }
2426
+
2427
+ function looksLikeGoalCreationAttempt(text: string): boolean {
2428
+ const candidatePattern = /\b(?:start|create|begin|new|another|goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b/gi;
2429
+ for (const match of text.matchAll(candidatePattern)) {
2430
+ const start = match.index ?? 0;
2431
+ const end = start + match[0].length;
2432
+ const window = sentenceWindowAround(text, start, end);
2433
+ if (isNegatedGoalAttemptWindow(window)) continue;
2434
+ if (/(?:\b(?:start|create|begin|new|another)\b.{0,80}\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b|\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b.{0,80}\b(?:start|create|begin|new|another)\b)/i.test(window)) return true;
2435
+ }
2436
+ return false;
2437
+ }
2438
+
2439
+ function looksLikeCreateGoalAttempt(text: string): boolean {
2440
+ const candidatePattern = /\bcreate_goal\s*(?:\(|\b)/gi;
2441
+ for (const match of text.matchAll(candidatePattern)) {
2442
+ const start = match.index ?? 0;
2443
+ const end = start + match[0].length;
2444
+ const window = sentenceWindowAround(text, start, end);
2445
+ if (/(?:\bno\s+create_goal\s+attempt\b|\b(?:do\s+not|don't|never|must\s+not|should\s+not|cannot|can't|not)\b.{0,40}\bcreate_goal\b|\bwithout\s+create_goal\b|\bfailed\s+to\s+call\s+create_goal\b|\bcreate_goal\s+(?:is|was)\s+(?:unavailable|not\s+available|not\s+called)\b)/i.test(window)) {
2446
+ continue;
2447
+ }
2448
+ if (/\b(?:call|calling|called|invoke|invoking|start|starting|create|creating|begin|beginning|attempt|attempting|try|trying|continue\s+to|proceed\s+to)\b.{0,80}\bcreate_goal\b/i.test(window)
2449
+ || /\bcreate_goal\b.{0,80}\b(?:payload|call|tool|now|next|follows|follow|start|starting|create|creating|begin|beginning|attempt|attempting)\b/i.test(window)
2450
+ || /\bcreate_goal\s*\(/i.test(match[0])) {
2451
+ return true;
2452
+ }
2453
+ }
2454
+ return false;
2455
+ }
2456
+
2457
+ function looksLikeCompletedGoalCleanupAttempt(text: string): boolean {
2458
+ return looksLikeGoalCreationAttempt(text) || looksLikeCreateGoalAttempt(text);
2459
+ }
2460
+ function hasFreshNativeGoalCleanupEvidence(text: string): boolean {
2461
+ return /\b(?:get_goal|Codex goal|native goal|active goal|thread goal)\b.{0,120}\b(?:reports?|returns?|shows?|status|state|still|attached|pending|cleanup|required|requires?)\b.{0,120}\b(?:complete|completed|attached|pending|cleanup|required|clear)\b/i.test(text)
2462
+ || /\b(?:complete|completed|attached|pending|cleanup|required|clear)\b.{0,120}\b(?:get_goal|Codex goal|native goal|active goal|thread goal)\b/i.test(text)
2463
+ || /\b(?:pending[-_ ]cleanup|native[-_ ]goal[-_ ]cleanup|completed[-_ ]codex[-_ ]goal[-_ ]cleanup)\b/i.test(text);
2464
+ }
2465
+
2466
+
2467
+ async function findCompletedGoalWorkflowCleanupNotice(cwd: string): Promise<string | null> {
2468
+ const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
2469
+ const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
2470
+ const ultragoals = Array.isArray(ultragoal?.goals) ? ultragoal.goals.map(safeObject) : [];
2471
+ if (safeString(aggregateCompletion.status) === "complete" || (ultragoals.length > 0 && ultragoals.every((goal) => safeString(goal.status) === "complete"))) {
2472
+ return buildCodexGoalTerminalCleanupNotice("Ultragoal completion");
2473
+ }
2474
+
2475
+ const performanceRoot = join(cwd, ".omx", "goals", "performance");
2476
+ for (const entry of await readdir(performanceRoot, { withFileTypes: true }).catch(() => [])) {
2477
+ if (!entry.isDirectory()) continue;
2478
+ const state = await readJsonIfExists(join(performanceRoot, entry.name, "state.json"));
2479
+ if (state?.workflow === "performance-goal" && safeString(state.status) === "complete") {
2480
+ return buildCodexGoalTerminalCleanupNotice("Performance-goal completion");
2481
+ }
2482
+ }
2483
+
2484
+ const autoresearchRoot = join(cwd, ".omx", "goals", "autoresearch");
2485
+ for (const entry of await readdir(autoresearchRoot, { withFileTypes: true }).catch(() => [])) {
2486
+ if (!entry.isDirectory()) continue;
2487
+ const mission = await readJsonIfExists(join(autoresearchRoot, entry.name, "mission.json"));
2488
+ if (mission?.workflow === "autoresearch-goal" && safeString(mission.status) === "complete") {
2489
+ return buildCodexGoalTerminalCleanupNotice("Autoresearch-goal completion");
2490
+ }
2491
+ }
2492
+
2493
+ return null;
2494
+ }
2495
+
2496
+ async function buildCompletedGoalCleanupPromptWarning(cwd: string, prompt: string): Promise<string | null> {
2497
+ if (!looksLikeCompletedGoalCleanupAttempt(prompt)) return null;
2498
+ const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
2499
+ if (!notice) return null;
2500
+ return `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
2501
+ }
2502
+
2503
+ async function buildCompletedGoalCleanupStopOutput(payload: CodexHookPayload, cwd: string): Promise<Record<string, unknown> | null> {
2504
+ const text = [
2505
+ safeString(payload.last_user_message ?? payload.lastUserMessage),
2506
+ safeString(payload.last_assistant_message ?? payload.lastAssistantMessage),
2507
+ ].join("\n");
2508
+ if (!looksLikeCompletedGoalCleanupAttempt(text)) return null;
2509
+ if (!hasFreshNativeGoalCleanupEvidence(text)) return null;
2510
+ const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
2511
+ if (!notice) return null;
2512
+ const systemMessage = `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
2513
+ return {
2514
+ decision: "block",
2515
+ reason: systemMessage,
2516
+ stopReason: "completed_codex_goal_cleanup_required",
2517
+ systemMessage,
2518
+ };
2519
+ }
2520
+
2297
2521
  async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
2298
2522
  const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
2299
2523
  const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
@@ -2545,6 +2769,144 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
2545
2769
  return safeString(payload.turn_id ?? payload.turnId).trim();
2546
2770
  }
2547
2771
 
2772
+ interface NativeSubagentCapacityBlocker {
2773
+ schema_version: 1;
2774
+ reason: "agent_thread_limit_reached";
2775
+ session_id?: string;
2776
+ thread_id?: string;
2777
+ turn_id?: string;
2778
+ tool_name?: string;
2779
+ error_summary: string;
2780
+ observed_at: string;
2781
+ expires_at: string;
2782
+ }
2783
+
2784
+ function nativeSubagentCapacityBlockerPath(stateDir: string): string {
2785
+ return join(stateDir, NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE);
2786
+ }
2787
+
2788
+ function stringifyUnknown(value: unknown): string {
2789
+ if (typeof value === "string") return value;
2790
+ if (value === undefined || value === null) return "";
2791
+ try {
2792
+ return JSON.stringify(value);
2793
+ } catch {
2794
+ return String(value);
2795
+ }
2796
+ }
2797
+
2798
+ function payloadEvidenceText(payload: CodexHookPayload): string {
2799
+ return [
2800
+ safeString(payload.tool_name),
2801
+ stringifyUnknown(payload.tool_response),
2802
+ stringifyUnknown(payload.response),
2803
+ stringifyUnknown(payload.error),
2804
+ stringifyUnknown(payload.message),
2805
+ ].filter(Boolean).join("\n");
2806
+ }
2807
+
2808
+ function isNativeSubagentCapacityFailure(payload: CodexHookPayload): boolean {
2809
+ const evidence = payloadEvidenceText(payload);
2810
+ if (!/\bagent thread limit reached\b/i.test(evidence)) return false;
2811
+ const toolName = safeString(payload.tool_name).trim();
2812
+ return !toolName || /(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName);
2813
+ }
2814
+
2815
+ function summarizeCapacityFailure(text: string): string {
2816
+ const normalized = text.replace(/\s+/g, " ").trim();
2817
+ if (!normalized) return "agent thread limit reached";
2818
+ const match = normalized.match(/[^.?!\n\r]*agent thread limit reached[^.?!\n\r]*/i);
2819
+ return (match?.[0] ?? normalized).slice(0, 500);
2820
+ }
2821
+
2822
+ async function recordNativeSubagentCapacityBlocker(
2823
+ cwd: string,
2824
+ stateDir: string,
2825
+ payload: CodexHookPayload,
2826
+ ): Promise<void> {
2827
+ if (!isNativeSubagentCapacityFailure(payload)) return;
2828
+ const nowMs = Date.now();
2829
+ const blocker: NativeSubagentCapacityBlocker = {
2830
+ schema_version: 1,
2831
+ reason: "agent_thread_limit_reached",
2832
+ ...(readPayloadSessionId(payload) ? { session_id: readPayloadSessionId(payload) } : {}),
2833
+ ...(readPayloadThreadId(payload) ? { thread_id: readPayloadThreadId(payload) } : {}),
2834
+ ...(readPayloadTurnId(payload) ? { turn_id: readPayloadTurnId(payload) } : {}),
2835
+ ...(safeString(payload.tool_name).trim() ? { tool_name: safeString(payload.tool_name).trim() } : {}),
2836
+ error_summary: summarizeCapacityFailure(payloadEvidenceText(payload)),
2837
+ observed_at: new Date(nowMs).toISOString(),
2838
+ expires_at: new Date(nowMs + NATIVE_SUBAGENT_CAPACITY_BLOCKER_TTL_MS).toISOString(),
2839
+ };
2840
+ await mkdir(stateDir, { recursive: true });
2841
+ await writeFile(nativeSubagentCapacityBlockerPath(stateDir), JSON.stringify({
2842
+ ...blocker,
2843
+ cwd,
2844
+ }, null, 2));
2845
+ }
2846
+
2847
+ function isFreshNativeSubagentCapacityBlocker(
2848
+ blocker: Record<string, unknown> | null,
2849
+ cwd: string,
2850
+ payload: CodexHookPayload,
2851
+ nowMs = Date.now(),
2852
+ ): blocker is NativeSubagentCapacityBlocker & Record<string, unknown> {
2853
+ if (!blocker) return false;
2854
+ if (safeString(blocker.reason) !== "agent_thread_limit_reached") return false;
2855
+ const expiresAtMs = Date.parse(safeString(blocker.expires_at));
2856
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) return false;
2857
+ const blockerCwd = safeString(blocker.cwd).trim();
2858
+ if (blockerCwd) {
2859
+ try {
2860
+ if (resolve(blockerCwd) !== resolve(cwd)) return false;
2861
+ } catch {
2862
+ return false;
2863
+ }
2864
+ }
2865
+ const blockerSessionId = safeString(blocker.session_id).trim();
2866
+ const payloadSessionId = readPayloadSessionId(payload);
2867
+ return !blockerSessionId || !payloadSessionId || blockerSessionId === payloadSessionId;
2868
+ }
2869
+
2870
+ function inputContainsCloseAgentRequest(value: unknown): boolean {
2871
+ if (typeof value === "string") return /\bclose_agent\b/i.test(value);
2872
+ if (!value || typeof value !== "object") return false;
2873
+ try {
2874
+ return /\bclose_agent\b/i.test(JSON.stringify(value));
2875
+ } catch {
2876
+ return false;
2877
+ }
2878
+ }
2879
+
2880
+ function isCloseAgentToolUse(payload: CodexHookPayload): boolean {
2881
+ const toolName = safeString(payload.tool_name).trim();
2882
+ if (/\bclose_agent\b/i.test(toolName)) return true;
2883
+ if (/multi_tool_use\.parallel/i.test(toolName) && inputContainsCloseAgentRequest(payload.tool_input)) return true;
2884
+ return inputContainsCloseAgentRequest(payload.tool_input) && /multi_agent|agent|tool_use/i.test(toolName);
2885
+ }
2886
+
2887
+ async function buildNativeSubagentCapacityCloseGuardOutput(
2888
+ payload: CodexHookPayload,
2889
+ cwd: string,
2890
+ stateDir: string,
2891
+ ): Promise<Record<string, unknown> | null> {
2892
+ if (!isCloseAgentToolUse(payload)) return null;
2893
+ const blocker = await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir));
2894
+ if (!isFreshNativeSubagentCapacityBlocker(blocker, cwd, payload)) return null;
2895
+
2896
+ const evidence = safeString(blocker.error_summary).trim() || "agent thread limit reached";
2897
+ return {
2898
+ decision: "block",
2899
+ reason: "Native subagent capacity was exhausted recently; model-level close_agent cleanup is blocked because close_agent can hang indefinitely on stale handles.",
2900
+ hookSpecificOutput: {
2901
+ hookEventName: "PreToolUse",
2902
+ additionalContext:
2903
+ `OMX blocked ${safeString(payload.tool_name).trim() || "close_agent"} before it could start: a recent native subagent capacity failure was recorded (${evidence}). `
2904
+ + "Do not call multi_agent_v1.close_agent, and do not batch close_agent through multi_tool_use.parallel, as stale native handles can hang the whole turn. "
2905
+ + "Treat this as a bounded capacity blocker: persist/report the blocker evidence, avoid further native subagent cleanup from the model turn, and recover via runtime-level cleanup or a fresh Codex session.",
2906
+ },
2907
+ };
2908
+ }
2909
+
2548
2910
  async function resolveInternalSessionIdForPayload(
2549
2911
  cwd: string,
2550
2912
  payloadSessionId: string,
@@ -2557,9 +2919,11 @@ async function resolveInternalSessionIdForPayload(
2557
2919
  if (!canonicalSessionId) return payloadSessionId;
2558
2920
 
2559
2921
  const nativeSessionId = safeString(currentSession?.native_session_id).trim();
2922
+ const ownerOmxSessionId = safeString(currentSession?.owner_omx_session_id).trim();
2560
2923
  if (!payloadSessionId) return canonicalSessionId;
2561
2924
  if (payloadSessionId === canonicalSessionId) return canonicalSessionId;
2562
2925
  if (nativeSessionId && payloadSessionId === nativeSessionId) return canonicalSessionId;
2926
+ if (ownerOmxSessionId && payloadSessionId === ownerOmxSessionId) return canonicalSessionId;
2563
2927
  return payloadSessionId;
2564
2928
  }
2565
2929
 
@@ -2603,6 +2967,7 @@ const RALPLAN_ALLOWED_WRITE_PREFIXES = [
2603
2967
  ".omx/plans",
2604
2968
  ".omx/specs",
2605
2969
  ".omx/state",
2970
+ ".beads",
2606
2971
  ] as const;
2607
2972
 
2608
2973
  const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
@@ -2653,6 +3018,10 @@ function canAutopilotSkillMirrorSupplyRalplanPhase(phase: string): boolean {
2653
3018
  return phase === "" || normalizeAutopilotPhase(phase) === "ralplan";
2654
3019
  }
2655
3020
 
3021
+ function isAutopilotReviewReworkPhase(phase: string): boolean {
3022
+ return normalizeAutopilotPhase(phase) === "rework";
3023
+ }
3024
+
2656
3025
  function hasExplicitExecutionHandoffSkill(
2657
3026
  state: SkillActiveStateLike | null,
2658
3027
  sessionId: string,
@@ -2692,6 +3061,138 @@ function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
2692
3061
  return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
2693
3062
  }
2694
3063
 
3064
+ interface RalplanBeadsCommandClassification {
3065
+ present: boolean;
3066
+ allowed: boolean;
3067
+ reason?: string;
3068
+ }
3069
+
3070
+ function shellTokenizeLiteralCommand(command: string): string[] | null {
3071
+ const tokens: string[] = [];
3072
+ let current = "";
3073
+ let quote: "'" | '"' | null = null;
3074
+ let escaping = false;
3075
+
3076
+ for (const char of command.trim()) {
3077
+ if (escaping) {
3078
+ current += char;
3079
+ escaping = false;
3080
+ continue;
3081
+ }
3082
+ if (quote === '"' && char === "\\") {
3083
+ escaping = true;
3084
+ continue;
3085
+ }
3086
+ if (quote) {
3087
+ if (char === quote) quote = null;
3088
+ else current += char;
3089
+ continue;
3090
+ }
3091
+ if (char === "'" || char === '"') {
3092
+ quote = char;
3093
+ continue;
3094
+ }
3095
+ if (/\s/.test(char)) {
3096
+ if (current) {
3097
+ tokens.push(current);
3098
+ current = "";
3099
+ }
3100
+ continue;
3101
+ }
3102
+ if (/[;&|<>`$(){}\n\r]/.test(char)) return null;
3103
+ current += char;
3104
+ }
3105
+
3106
+ if (escaping || quote) return null;
3107
+ if (current) tokens.push(current);
3108
+ return tokens;
3109
+ }
3110
+
3111
+ function findLiteralBdExecutableIndex(tokens: string[]): number {
3112
+ if (tokens[0] === "bd") return 0;
3113
+ if (tokens[0] === "command" || tokens[0] === "builtin" || tokens[0] === "exec" || tokens[0] === "nohup") {
3114
+ return tokens[1] === "bd" ? 1 : -1;
3115
+ }
3116
+ if (tokens[0] !== "env") return -1;
3117
+
3118
+ for (let index = 1; index < tokens.length; index += 1) {
3119
+ const token = tokens[index] ?? "";
3120
+ if (token === "bd") return index;
3121
+ if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
3122
+ if (token.startsWith("-")) continue;
3123
+ return -1;
3124
+ }
3125
+ return -1;
3126
+ }
3127
+
3128
+ function isAllowedRalplanBeadsDbPath(cwd: string, rawPath: string): boolean {
3129
+ const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
3130
+ if (!trimmed || trimmed.includes("\0")) return false;
3131
+ let relativePath: string;
3132
+ try {
3133
+ const absolute = resolve(cwd, trimmed);
3134
+ relativePath = relative(cwd, absolute).replace(/\\/g, "/");
3135
+ } catch {
3136
+ return false;
3137
+ }
3138
+ return relativePath.startsWith(".beads/") && relativePath.length > ".beads/".length;
3139
+ }
3140
+
3141
+ function classifyRalplanBeadsMetadataCommand(cwd: string, command: string): RalplanBeadsCommandClassification {
3142
+ const trimmedCommand = command.trim();
3143
+ const startsWithBd = /^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"$`]*"|'[^']*'|[^\s"'$`;&|<>]+)\s+)*bd(?:\s|$)/.test(trimmedCommand);
3144
+ const hasCompoundBd = /[;&|()]\s*bd(?:\s|$)/.test(command);
3145
+ const tokens = shellTokenizeLiteralCommand(command);
3146
+ const bdExecutableIndex = tokens ? findLiteralBdExecutableIndex(tokens) : -1;
3147
+ if (!startsWithBd && !hasCompoundBd && bdExecutableIndex === -1) return { present: false, allowed: false };
3148
+
3149
+ if (!tokens || bdExecutableIndex !== 0) {
3150
+ return { present: true, allowed: false, reason: "Beads tracker command must be a single literal bd invocation" };
3151
+ }
3152
+
3153
+ let dbPath = "";
3154
+ let dbValueIndex = -1;
3155
+ for (let index = 1; index < tokens.length; index += 1) {
3156
+ const token = tokens[index] ?? "";
3157
+ if (token === "--db") {
3158
+ dbPath = tokens[index + 1] ?? "";
3159
+ dbValueIndex = index + 1;
3160
+ break;
3161
+ }
3162
+ if (token.startsWith("--db=")) {
3163
+ dbPath = token.slice("--db=".length);
3164
+ dbValueIndex = index;
3165
+ break;
3166
+ }
3167
+ }
3168
+
3169
+ if (!dbPath) {
3170
+ return { present: true, allowed: false, reason: "Beads tracker command is missing a literal --db .beads/<db> target" };
3171
+ }
3172
+ if (!isAllowedRalplanBeadsDbPath(cwd, dbPath)) {
3173
+ return { present: true, allowed: false, reason: `Beads tracker db target ${dbPath} is outside repo-local .beads metadata` };
3174
+ }
3175
+
3176
+ const operationTokens = tokens
3177
+ .slice(dbValueIndex + 1)
3178
+ .filter((token) => token && !token.startsWith("-"));
3179
+ const operation = operationTokens[0] ?? "";
3180
+ const suboperation = operationTokens[1] ?? "";
3181
+ if (["create", "update", "edit", "close", "reopen", "status", "dep"].includes(operation)) {
3182
+ return { present: true, allowed: true };
3183
+ }
3184
+ if (operation === "comments" && suboperation === "add") {
3185
+ return { present: true, allowed: true };
3186
+ }
3187
+ return {
3188
+ present: true,
3189
+ allowed: false,
3190
+ reason: operation
3191
+ ? `Beads tracker operation ${operation}${suboperation ? ` ${suboperation}` : ""} is not allowed during planning`
3192
+ : "Beads tracker command is missing an allowed metadata operation",
3193
+ };
3194
+ }
3195
+
2695
3196
  function readPreToolUseCommand(payload: CodexHookPayload): string {
2696
3197
  const toolInput = safeObject(payload.tool_input);
2697
3198
  return safeString(toolInput.command).trim();
@@ -2805,6 +3306,33 @@ function extractDeepInterviewCommandWriteTargets(command: string): string[] {
2805
3306
  }
2806
3307
  return targets;
2807
3308
  }
3309
+ function formatPlanningWriteBlockDetail(
3310
+ operationClass: string,
3311
+ target: string | undefined,
3312
+ allowedPrefixes: readonly string[],
3313
+ ): string {
3314
+ const targetDetail = target ? `target ${target}` : "target <unresolved>";
3315
+ return `${operationClass} ${targetDetail} is not under allowed planning artifact paths (${allowedPrefixes.join(", ")})`;
3316
+ }
3317
+
3318
+ function isUnresolvedVariableTarget(target: string): boolean {
3319
+ const normalized = target.trim().replace(/^['"]|['"]$/g, "");
3320
+ return /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(normalized);
3321
+ }
3322
+
3323
+
3324
+ function describeImplementationToolBlock(
3325
+ toolName: string,
3326
+ blockedPath: string | undefined,
3327
+ pathCount: number,
3328
+ ): string {
3329
+ if (pathCount === 0) {
3330
+ const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target extraction failed" : `${toolName} path`;
3331
+ return `${operationClass} target <unresolved>; only planning artifact paths are allowed (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3332
+ }
3333
+ const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target" : `${toolName} path`;
3334
+ return formatPlanningWriteBlockDetail(operationClass, blockedPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
3335
+ }
2808
3336
 
2809
3337
  function isAllowedDeepInterviewBashWrite(cwd: string, command: string): boolean {
2810
3338
  if (!commandHasDeepInterviewWriteIntent(command)) return true;
@@ -2899,6 +3427,7 @@ async function readActiveRalplanStateForPreToolUse(
2899
3427
  ));
2900
3428
  if (!hasActiveAutopilotSkill) return null;
2901
3429
  const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
3430
+ if (isAutopilotReviewReworkPhase(autopilotStatePhase)) return null;
2902
3431
  if (!canAutopilotSkillMirrorSupplyRalplanPhase(autopilotStatePhase)) return null;
2903
3432
  const hasRalplanScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
2904
3433
  entry.skill === "autopilot"
@@ -2910,10 +3439,37 @@ async function readActiveRalplanStateForPreToolUse(
2910
3439
  }
2911
3440
 
2912
3441
  function isAllowedRalplanBashWrite(cwd: string, command: string): boolean {
3442
+ const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
3443
+ const targets = extractDeepInterviewCommandWriteTargets(command);
3444
+ const hasAllowedTargets = targets.length > 0
3445
+ && targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
3446
+
3447
+ if (beadsCommand.present) {
3448
+ return beadsCommand.allowed && (targets.length === 0 || hasAllowedTargets);
3449
+ }
2913
3450
  if (!commandHasDeepInterviewWriteIntent(command)) return true;
2914
3451
  if (/\bomx\s+(?:state\s+(?:write|read|clear)|question)\b/.test(command)) return true;
3452
+ return hasAllowedTargets;
3453
+ }
3454
+
3455
+ function buildRalplanBashBlockedDetail(cwd: string, command: string): string {
2915
3456
  const targets = extractDeepInterviewCommandWriteTargets(command);
2916
- return targets.length > 0 && targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
3457
+ const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
3458
+ if (blockedTarget && isUnresolvedVariableTarget(blockedTarget)) {
3459
+ return `unresolved Bash write target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3460
+ }
3461
+ if (blockedTarget) {
3462
+ const operationClass = /\btee\s+(?:-a\s+)?/.test(command) ? "Bash tee write" : "Bash redirect write";
3463
+ return `${operationClass} target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3464
+ }
3465
+ const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
3466
+ if (beadsCommand.present && !beadsCommand.allowed) {
3467
+ return beadsCommand.reason ?? "Beads tracker command is not an allowed planning metadata mutation";
3468
+ }
3469
+ if (beadsCommand.present) {
3470
+ return "Beads tracker command also performs an implementation write outside allowed planning metadata";
3471
+ }
3472
+ return "Bash write intent did not identify an allowed planning artifact path or metadata path";
2917
3473
  }
2918
3474
 
2919
3475
  async function buildRalplanPreToolUseBoundaryOutput(
@@ -2936,21 +3492,18 @@ async function buildRalplanPreToolUseBoundaryOutput(
2936
3492
  if (toolName === "Bash") {
2937
3493
  blocked = !isAllowedRalplanBashWrite(cwd, command);
2938
3494
  if (blocked) {
2939
- const targets = extractDeepInterviewCommandWriteTargets(command);
2940
- const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
2941
- blockedDetail = blockedTarget
2942
- ? `write target ${blockedTarget} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`
2943
- : "Bash write intent did not identify an allowed planning artifact path";
3495
+ blockedDetail = buildRalplanBashBlockedDetail(cwd, command);
2944
3496
  }
2945
3497
  } else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
2946
- if (pathCandidates.length === 0) {
3498
+ const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
3499
+ if (toolPathCandidates.length === 0) {
2947
3500
  blocked = true;
2948
- blockedDetail = `${toolName} did not include a file path; only planning artifact paths are allowed`;
3501
+ blockedDetail = describeImplementationToolBlock(toolName, undefined, toolPathCandidates.length);
2949
3502
  } else {
2950
- const blockedPath = pathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
3503
+ const blockedPath = toolPathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
2951
3504
  blocked = blockedPath !== undefined;
2952
3505
  if (blockedPath !== undefined) {
2953
- blockedDetail = `path ${blockedPath} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3506
+ blockedDetail = describeImplementationToolBlock(toolName, blockedPath, toolPathCandidates.length);
2954
3507
  }
2955
3508
  }
2956
3509
  }
@@ -2970,7 +3523,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
2970
3523
  hookEventName: "PreToolUse",
2971
3524
  additionalContext:
2972
3525
  `${planningModeDescription}. `
2973
- + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, or required `.omx/state/` files. "
3526
+ + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
2974
3527
  + "Do not edit implementation files or run implementation-focused writes from planning phases. "
2975
3528
  + `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
2976
3529
  },
@@ -3060,6 +3613,22 @@ function modeStateMatchesSkillStopContext(
3060
3613
  return true;
3061
3614
  }
3062
3615
 
3616
+ function modeStateHasExplicitMatchingCwd(state: Record<string, unknown>, cwd: string): boolean {
3617
+ const stateCwd = safeString(
3618
+ state.cwd
3619
+ ?? state.workingDirectory
3620
+ ?? state.working_directory
3621
+ ?? state.project_path,
3622
+ ).trim();
3623
+ if (!stateCwd) return false;
3624
+
3625
+ try {
3626
+ return resolve(stateCwd) === resolve(cwd);
3627
+ } catch {
3628
+ return false;
3629
+ }
3630
+ }
3631
+
3063
3632
  async function readBlockingSkillForStop(
3064
3633
  cwd: string,
3065
3634
  stateDir: string,
@@ -3178,6 +3747,54 @@ function rootModeStateIsCanonicalForStopContext(
3178
3747
  return true;
3179
3748
  }
3180
3749
 
3750
+ function hasExplicitSessionScope(state: Record<string, unknown>): boolean {
3751
+ return safeString(
3752
+ state.owner_omx_session_id
3753
+ ?? state.session_id
3754
+ ?? state.codex_session_id
3755
+ ?? state.owner_codex_session_id,
3756
+ ).trim() !== "";
3757
+ }
3758
+
3759
+ async function readStateTimestampMs(state: Record<string, unknown>, path: string): Promise<number | null> {
3760
+ return parseTimestampMs(state.updated_at)
3761
+ ?? parseTimestampMs(state.completed_at)
3762
+ ?? parseTimestampMs(state.created_at)
3763
+ ?? await stat(path).then((info) => info.mtimeMs, () => null);
3764
+ }
3765
+
3766
+ async function unscopedRootRalplanStateIsNewerTerminalPlanningCompletion(
3767
+ rootState: Record<string, unknown>,
3768
+ rootPath: string,
3769
+ sessionPath: string,
3770
+ cwd: string,
3771
+ threadId: string,
3772
+ ): Promise<boolean> {
3773
+ if (hasExplicitSessionScope(rootState)) return false;
3774
+
3775
+ const stateThreadId = safeString(rootState.owner_codex_thread_id ?? rootState.thread_id).trim();
3776
+ if (threadId && stateThreadId && stateThreadId !== threadId) return false;
3777
+
3778
+ if (!modeStateHasExplicitMatchingCwd(rootState, cwd)) return false;
3779
+
3780
+ const phase = safeString(rootState.current_phase ?? rootState.currentPhase).trim().toLowerCase();
3781
+ if (phase !== "complete" && phase !== "completed") return false;
3782
+
3783
+ const planningComplete = rootState.planning_complete === true || rootState.planningComplete === true;
3784
+ const latestPlanPath = safeString(rootState.latest_plan_path ?? rootState.latestPlanPath).trim();
3785
+ if (!planningComplete || !latestPlanPath) return false;
3786
+
3787
+ const sessionState = await readJsonIfExists(sessionPath);
3788
+ if (!sessionState) return false;
3789
+ const sessionPhase = safeString(sessionState.current_phase ?? sessionState.currentPhase).trim().toLowerCase();
3790
+ if (sessionPhase && TERMINAL_MODE_PHASES.has(sessionPhase)) return false;
3791
+
3792
+ const rootTimestamp = await readStateTimestampMs(rootState, rootPath);
3793
+ const sessionTimestamp = await readStateTimestampMs(sessionState, sessionPath);
3794
+ if (rootTimestamp === null || sessionTimestamp === null) return false;
3795
+ return rootTimestamp > sessionTimestamp;
3796
+ }
3797
+
3181
3798
  async function shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
3182
3799
  cwd: string,
3183
3800
  stateDir: string,
@@ -3185,11 +3802,24 @@ async function shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
3185
3802
  sessionId: string,
3186
3803
  threadId: string,
3187
3804
  ): Promise<boolean> {
3188
- const rootModeState = await readJsonIfExists(join(stateDir, `${skill}-state.json`));
3805
+ const rootModeStatePath = join(stateDir, `${skill}-state.json`);
3806
+ const rootModeState = await readJsonIfExists(rootModeStatePath);
3189
3807
  if (!rootModeState) return false;
3190
- if (!rootModeStateIsCanonicalForStopContext(rootModeState, cwd, sessionId, threadId)) return false;
3191
3808
  if (!isTerminalOrInactiveModeState(rootModeState)) return false;
3192
3809
 
3810
+ const canonicalRoot = rootModeStateIsCanonicalForStopContext(rootModeState, cwd, sessionId, threadId)
3811
+ && (skill !== "ralplan" || modeStateHasExplicitMatchingCwd(rootModeState, cwd));
3812
+ const freshUnscopedRoot = canonicalRoot || skill !== "ralplan"
3813
+ ? false
3814
+ : await unscopedRootRalplanStateIsNewerTerminalPlanningCompletion(
3815
+ rootModeState,
3816
+ rootModeStatePath,
3817
+ join(stateDir, "sessions", sessionId, `${skill}-state.json`),
3818
+ cwd,
3819
+ threadId,
3820
+ );
3821
+ if (!canonicalRoot && !freshUnscopedRoot) return false;
3822
+
3193
3823
  const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
3194
3824
  const rootSkillState = await readSkillActiveState(rootPath);
3195
3825
  return rootSkillStateHasNoActiveSkillForStopContext(rootSkillState, skill, sessionId, threadId);
@@ -4361,7 +4991,8 @@ export async function dispatchCodexNativeHook(
4361
4991
 
4362
4992
  if (hookEventName === "UserPromptSubmit") {
4363
4993
  const prompt = readPromptText(payload);
4364
- goalWorkflowAdditionalContext = await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
4994
+ goalWorkflowAdditionalContext = await buildCompletedGoalCleanupPromptWarning(cwd, prompt).catch(() => null)
4995
+ ?? await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
4365
4996
  ultragoalSteeringAdditionalContext = prompt && !isSubagentPromptSubmit
4366
4997
  ? await applyUserPromptUltragoalSteering(cwd, prompt).catch((error) => `OMX native UserPromptSubmit rejected bounded .omx/ultragoal steering for G002-cli-and-prompt-submit-bridge: ${error instanceof Error ? error.message : String(error)}`)
4367
4998
  : null;
@@ -4534,8 +5165,10 @@ export async function dispatchCodexNativeHook(
4534
5165
  : "";
4535
5166
  outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
4536
5167
  ?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
5168
+ ?? await buildNativeSubagentCapacityCloseGuardOutput(payload, cwd, stateDir)
4537
5169
  ?? buildNativePreToolUseOutput(payload);
4538
5170
  } else if (hookEventName === "PostToolUse") {
5171
+ await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => {});
4539
5172
  if (detectMcpTransportFailure(payload)) {
4540
5173
  await markTeamTransportFailure(cwd, payload);
4541
5174
  }
@@ -4544,7 +5177,7 @@ export async function dispatchCodexNativeHook(
4544
5177
  } else if (hookEventName === "Stop") {
4545
5178
  outputJson = await buildStopHookOutput(payload, cwd, stateDir, {
4546
5179
  skipRalphStopBlock: isSubagentStop,
4547
- });
5180
+ }) ?? await buildCompletedGoalCleanupStopOutput(payload, cwd);
4548
5181
  }
4549
5182
 
4550
5183
  return {
@@ -4657,6 +5290,9 @@ function buildMalformedStdinHookOutput(
4657
5290
  const systemMessage =
4658
5291
  `${reason} stdin JSON parsing failed inside codex-native-hook: ${parseError.message}.`;
4659
5292
  const inferredHookEventName = inferHookEventNameFromMalformedInput(rawInput);
5293
+ if (inferredHookEventName === "PreToolUse") {
5294
+ return { systemMessage };
5295
+ }
4660
5296
  if (inferredHookEventName === "Stop" || (!inferredHookEventName && hasNativeStopRuntimeSurface(cwd))) {
4661
5297
  return {
4662
5298
  decision: "block",
@@ -4699,11 +5335,14 @@ async function buildOversizedStdinHookOutput(
4699
5335
  rawHookEventName: CodexHookEventName | null,
4700
5336
  cwd: string,
4701
5337
  ): Promise<Record<string, unknown>> {
5338
+ const systemMessage =
5339
+ `OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
5340
+ if (rawHookEventName === "PreToolUse") {
5341
+ return { systemMessage };
5342
+ }
4702
5343
  if (rawHookEventName === "Stop") {
4703
5344
  return await buildOversizedStopActiveWorkflowOutput(cwd) ?? {};
4704
5345
  }
4705
- const systemMessage =
4706
- `OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
4707
5346
  return {
4708
5347
  continue: false,
4709
5348
  stopReason: "native_hook_stdin_oversized",
@@ -4817,8 +5456,8 @@ export async function runCodexNativeHookCli(): Promise<void> {
4817
5456
 
4818
5457
  const result = await dispatchCodexNativeHook(payload);
4819
5458
  if (result.outputJson) {
4820
- writeNativeHookJsonStdout(result.outputJson);
4821
- } else if (result.hookEventName === "Stop") {
5459
+ writeNativeHookJsonStdout(sanitizeCodexHookOutput(result.hookEventName, result.outputJson) ?? {});
5460
+ } else if (result.hookEventName !== "PreCompact" && result.hookEventName !== "PostCompact") {
4822
5461
  writeNativeHookJsonStdout({});
4823
5462
  }
4824
5463
  } catch (error) {