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
@@ -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,
@@ -61,6 +62,7 @@ import {
61
62
  SLOPPY_FALLBACK_PHRASE_PATTERNS,
62
63
  buildNativePostToolUseOutput,
63
64
  buildNativePreToolUseOutput,
65
+ commandInvokesApplyPatch,
64
66
  detectMcpTransportFailure,
65
67
  hasAnyPattern,
66
68
  } from "./codex-native-pre-post.js";
@@ -86,6 +88,9 @@ import { readAutoresearchCompletionStatus, readAutoresearchModeStateForActiveDec
86
88
  import { normalizeAutopilotPhase } from "../autopilot/fsm.js";
87
89
  import { readRunState } from "../runtime/run-state.js";
88
90
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
91
+ import {
92
+ buildCodexGoalTerminalCleanupNotice,
93
+ } from "../goal-workflows/codex-goal-snapshot.js";
89
94
  import { getRunContinuationSnapshot, shouldContinueRun } from "../runtime/run-loop.js";
90
95
  import {
91
96
  parseUltragoalSteeringDirective,
@@ -146,6 +151,8 @@ const SKILL_STOP_BLOCKERS = new Set(["ralplan"]);
146
151
  const TEAM_STOP_BLOCKING_TASK_STATUSES = new Set(["pending", "in_progress", "blocked"]);
147
152
  const TEAM_WORKER_TERMINAL_RUN_STATES = new Set(["done", "complete", "completed", "failed", "stopped", "cancelled"]);
148
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;
149
156
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS = 8;
150
157
  const RALPH_ORPHANED_STARTING_STALE_MS = 15 * 60_000;
151
158
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_IDLE_MS = 10 * 60_000;
@@ -548,6 +555,23 @@ function readHookEventName(payload: CodexHookPayload): CodexHookEventName | null
548
555
  return null;
549
556
  }
550
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
+
551
575
  export function mapCodexHookEventToOmxEvent(
552
576
  hookEventName: CodexHookEventName | null,
553
577
  ): string | null {
@@ -2226,6 +2250,76 @@ function isStopExempt(payload: CodexHookPayload): boolean {
2226
2250
  );
2227
2251
  }
2228
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
+
2229
2323
  async function buildModeBasedStopOutput(
2230
2324
  mode: "autopilot" | "ultrawork" | "ultraqa",
2231
2325
  cwd: string,
@@ -2237,17 +2331,38 @@ async function buildModeBasedStopOutput(
2237
2331
  if (mode === "autopilot" && await readAutopilotDeepInterviewQuestionWaitState(cwd, sessionId)) {
2238
2332
  return null;
2239
2333
  }
2240
- const state = await readModeStateForActiveDecision(mode, sessionId?.trim() || undefined, cwd);
2334
+ const sourcedState = await readModeStateWithStopSource(mode, cwd, sessionId);
2335
+ const state = sourcedState?.state ?? null;
2241
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;
2242
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}`;
2243
2356
  const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
2244
- ? "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state."
2245
- : `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}).`;
2246
2359
  return {
2247
2360
  decision: "block",
2248
- 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.`,
2249
2362
  stopReason: `${mode}_${phase}`,
2250
2363
  systemMessage,
2364
+ statePath,
2365
+ canonicalDisagreement,
2251
2366
  };
2252
2367
  }
2253
2368
 
@@ -2293,6 +2408,63 @@ function reportsBlockedUltragoalCompletedAggregateMicrogoalLoop(goal: Record<str
2293
2408
  && /\b(?:unreconcilable|mismatch|loop|already complete|already completed|blocks?)\b/i.test(evidence);
2294
2409
  }
2295
2410
 
2411
+ function looksLikeNewGoalPrompt(text: string): boolean {
2412
+ return /(?:\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(text);
2413
+ }
2414
+
2415
+ async function findCompletedGoalWorkflowCleanupNotice(cwd: string): Promise<string | null> {
2416
+ const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
2417
+ const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
2418
+ const ultragoals = Array.isArray(ultragoal?.goals) ? ultragoal.goals.map(safeObject) : [];
2419
+ if (safeString(aggregateCompletion.status) === "complete" || (ultragoals.length > 0 && ultragoals.every((goal) => safeString(goal.status) === "complete"))) {
2420
+ return buildCodexGoalTerminalCleanupNotice("Ultragoal completion");
2421
+ }
2422
+
2423
+ const performanceRoot = join(cwd, ".omx", "goals", "performance");
2424
+ for (const entry of await readdir(performanceRoot, { withFileTypes: true }).catch(() => [])) {
2425
+ if (!entry.isDirectory()) continue;
2426
+ const state = await readJsonIfExists(join(performanceRoot, entry.name, "state.json"));
2427
+ if (state?.workflow === "performance-goal" && safeString(state.status) === "complete") {
2428
+ return buildCodexGoalTerminalCleanupNotice("Performance-goal completion");
2429
+ }
2430
+ }
2431
+
2432
+ const autoresearchRoot = join(cwd, ".omx", "goals", "autoresearch");
2433
+ for (const entry of await readdir(autoresearchRoot, { withFileTypes: true }).catch(() => [])) {
2434
+ if (!entry.isDirectory()) continue;
2435
+ const mission = await readJsonIfExists(join(autoresearchRoot, entry.name, "mission.json"));
2436
+ if (mission?.workflow === "autoresearch-goal" && safeString(mission.status) === "complete") {
2437
+ return buildCodexGoalTerminalCleanupNotice("Autoresearch-goal completion");
2438
+ }
2439
+ }
2440
+
2441
+ return null;
2442
+ }
2443
+
2444
+ async function buildCompletedGoalCleanupPromptWarning(cwd: string, prompt: string): Promise<string | null> {
2445
+ if (!looksLikeNewGoalPrompt(prompt)) return null;
2446
+ const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
2447
+ if (!notice) return null;
2448
+ return `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
2449
+ }
2450
+
2451
+ async function buildCompletedGoalCleanupStopOutput(payload: CodexHookPayload, cwd: string): Promise<Record<string, unknown> | null> {
2452
+ const text = [
2453
+ safeString(payload.last_user_message ?? payload.lastUserMessage),
2454
+ safeString(payload.last_assistant_message ?? payload.lastAssistantMessage),
2455
+ ].join("\n");
2456
+ if (!looksLikeNewGoalPrompt(text)) return null;
2457
+ const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
2458
+ if (!notice) return null;
2459
+ const systemMessage = `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
2460
+ return {
2461
+ decision: "block",
2462
+ reason: systemMessage,
2463
+ stopReason: "completed_codex_goal_cleanup_required",
2464
+ systemMessage,
2465
+ };
2466
+ }
2467
+
2296
2468
  async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
2297
2469
  const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
2298
2470
  const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
@@ -2544,6 +2716,144 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
2544
2716
  return safeString(payload.turn_id ?? payload.turnId).trim();
2545
2717
  }
2546
2718
 
2719
+ interface NativeSubagentCapacityBlocker {
2720
+ schema_version: 1;
2721
+ reason: "agent_thread_limit_reached";
2722
+ session_id?: string;
2723
+ thread_id?: string;
2724
+ turn_id?: string;
2725
+ tool_name?: string;
2726
+ error_summary: string;
2727
+ observed_at: string;
2728
+ expires_at: string;
2729
+ }
2730
+
2731
+ function nativeSubagentCapacityBlockerPath(stateDir: string): string {
2732
+ return join(stateDir, NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE);
2733
+ }
2734
+
2735
+ function stringifyUnknown(value: unknown): string {
2736
+ if (typeof value === "string") return value;
2737
+ if (value === undefined || value === null) return "";
2738
+ try {
2739
+ return JSON.stringify(value);
2740
+ } catch {
2741
+ return String(value);
2742
+ }
2743
+ }
2744
+
2745
+ function payloadEvidenceText(payload: CodexHookPayload): string {
2746
+ return [
2747
+ safeString(payload.tool_name),
2748
+ stringifyUnknown(payload.tool_response),
2749
+ stringifyUnknown(payload.response),
2750
+ stringifyUnknown(payload.error),
2751
+ stringifyUnknown(payload.message),
2752
+ ].filter(Boolean).join("\n");
2753
+ }
2754
+
2755
+ function isNativeSubagentCapacityFailure(payload: CodexHookPayload): boolean {
2756
+ const evidence = payloadEvidenceText(payload);
2757
+ if (!/\bagent thread limit reached\b/i.test(evidence)) return false;
2758
+ const toolName = safeString(payload.tool_name).trim();
2759
+ return !toolName || /(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName);
2760
+ }
2761
+
2762
+ function summarizeCapacityFailure(text: string): string {
2763
+ const normalized = text.replace(/\s+/g, " ").trim();
2764
+ if (!normalized) return "agent thread limit reached";
2765
+ const match = normalized.match(/[^.?!\n\r]*agent thread limit reached[^.?!\n\r]*/i);
2766
+ return (match?.[0] ?? normalized).slice(0, 500);
2767
+ }
2768
+
2769
+ async function recordNativeSubagentCapacityBlocker(
2770
+ cwd: string,
2771
+ stateDir: string,
2772
+ payload: CodexHookPayload,
2773
+ ): Promise<void> {
2774
+ if (!isNativeSubagentCapacityFailure(payload)) return;
2775
+ const nowMs = Date.now();
2776
+ const blocker: NativeSubagentCapacityBlocker = {
2777
+ schema_version: 1,
2778
+ reason: "agent_thread_limit_reached",
2779
+ ...(readPayloadSessionId(payload) ? { session_id: readPayloadSessionId(payload) } : {}),
2780
+ ...(readPayloadThreadId(payload) ? { thread_id: readPayloadThreadId(payload) } : {}),
2781
+ ...(readPayloadTurnId(payload) ? { turn_id: readPayloadTurnId(payload) } : {}),
2782
+ ...(safeString(payload.tool_name).trim() ? { tool_name: safeString(payload.tool_name).trim() } : {}),
2783
+ error_summary: summarizeCapacityFailure(payloadEvidenceText(payload)),
2784
+ observed_at: new Date(nowMs).toISOString(),
2785
+ expires_at: new Date(nowMs + NATIVE_SUBAGENT_CAPACITY_BLOCKER_TTL_MS).toISOString(),
2786
+ };
2787
+ await mkdir(stateDir, { recursive: true });
2788
+ await writeFile(nativeSubagentCapacityBlockerPath(stateDir), JSON.stringify({
2789
+ ...blocker,
2790
+ cwd,
2791
+ }, null, 2));
2792
+ }
2793
+
2794
+ function isFreshNativeSubagentCapacityBlocker(
2795
+ blocker: Record<string, unknown> | null,
2796
+ cwd: string,
2797
+ payload: CodexHookPayload,
2798
+ nowMs = Date.now(),
2799
+ ): blocker is NativeSubagentCapacityBlocker & Record<string, unknown> {
2800
+ if (!blocker) return false;
2801
+ if (safeString(blocker.reason) !== "agent_thread_limit_reached") return false;
2802
+ const expiresAtMs = Date.parse(safeString(blocker.expires_at));
2803
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) return false;
2804
+ const blockerCwd = safeString(blocker.cwd).trim();
2805
+ if (blockerCwd) {
2806
+ try {
2807
+ if (resolve(blockerCwd) !== resolve(cwd)) return false;
2808
+ } catch {
2809
+ return false;
2810
+ }
2811
+ }
2812
+ const blockerSessionId = safeString(blocker.session_id).trim();
2813
+ const payloadSessionId = readPayloadSessionId(payload);
2814
+ return !blockerSessionId || !payloadSessionId || blockerSessionId === payloadSessionId;
2815
+ }
2816
+
2817
+ function inputContainsCloseAgentRequest(value: unknown): boolean {
2818
+ if (typeof value === "string") return /\bclose_agent\b/i.test(value);
2819
+ if (!value || typeof value !== "object") return false;
2820
+ try {
2821
+ return /\bclose_agent\b/i.test(JSON.stringify(value));
2822
+ } catch {
2823
+ return false;
2824
+ }
2825
+ }
2826
+
2827
+ function isCloseAgentToolUse(payload: CodexHookPayload): boolean {
2828
+ const toolName = safeString(payload.tool_name).trim();
2829
+ if (/\bclose_agent\b/i.test(toolName)) return true;
2830
+ if (/multi_tool_use\.parallel/i.test(toolName) && inputContainsCloseAgentRequest(payload.tool_input)) return true;
2831
+ return inputContainsCloseAgentRequest(payload.tool_input) && /multi_agent|agent|tool_use/i.test(toolName);
2832
+ }
2833
+
2834
+ async function buildNativeSubagentCapacityCloseGuardOutput(
2835
+ payload: CodexHookPayload,
2836
+ cwd: string,
2837
+ stateDir: string,
2838
+ ): Promise<Record<string, unknown> | null> {
2839
+ if (!isCloseAgentToolUse(payload)) return null;
2840
+ const blocker = await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir));
2841
+ if (!isFreshNativeSubagentCapacityBlocker(blocker, cwd, payload)) return null;
2842
+
2843
+ const evidence = safeString(blocker.error_summary).trim() || "agent thread limit reached";
2844
+ return {
2845
+ decision: "block",
2846
+ reason: "Native subagent capacity was exhausted recently; model-level close_agent cleanup is blocked because close_agent can hang indefinitely on stale handles.",
2847
+ hookSpecificOutput: {
2848
+ hookEventName: "PreToolUse",
2849
+ additionalContext:
2850
+ `OMX blocked ${safeString(payload.tool_name).trim() || "close_agent"} before it could start: a recent native subagent capacity failure was recorded (${evidence}). `
2851
+ + "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. "
2852
+ + "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.",
2853
+ },
2854
+ };
2855
+ }
2856
+
2547
2857
  async function resolveInternalSessionIdForPayload(
2548
2858
  cwd: string,
2549
2859
  payloadSessionId: string,
@@ -2602,6 +2912,7 @@ const RALPLAN_ALLOWED_WRITE_PREFIXES = [
2602
2912
  ".omx/plans",
2603
2913
  ".omx/specs",
2604
2914
  ".omx/state",
2915
+ ".beads",
2605
2916
  ] as const;
2606
2917
 
2607
2918
  const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
@@ -2652,6 +2963,10 @@ function canAutopilotSkillMirrorSupplyRalplanPhase(phase: string): boolean {
2652
2963
  return phase === "" || normalizeAutopilotPhase(phase) === "ralplan";
2653
2964
  }
2654
2965
 
2966
+ function isAutopilotReviewReworkPhase(phase: string): boolean {
2967
+ return normalizeAutopilotPhase(phase) === "rework";
2968
+ }
2969
+
2655
2970
  function hasExplicitExecutionHandoffSkill(
2656
2971
  state: SkillActiveStateLike | null,
2657
2972
  sessionId: string,
@@ -2691,6 +3006,138 @@ function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
2691
3006
  return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
2692
3007
  }
2693
3008
 
3009
+ interface RalplanBeadsCommandClassification {
3010
+ present: boolean;
3011
+ allowed: boolean;
3012
+ reason?: string;
3013
+ }
3014
+
3015
+ function shellTokenizeLiteralCommand(command: string): string[] | null {
3016
+ const tokens: string[] = [];
3017
+ let current = "";
3018
+ let quote: "'" | '"' | null = null;
3019
+ let escaping = false;
3020
+
3021
+ for (const char of command.trim()) {
3022
+ if (escaping) {
3023
+ current += char;
3024
+ escaping = false;
3025
+ continue;
3026
+ }
3027
+ if (quote === '"' && char === "\\") {
3028
+ escaping = true;
3029
+ continue;
3030
+ }
3031
+ if (quote) {
3032
+ if (char === quote) quote = null;
3033
+ else current += char;
3034
+ continue;
3035
+ }
3036
+ if (char === "'" || char === '"') {
3037
+ quote = char;
3038
+ continue;
3039
+ }
3040
+ if (/\s/.test(char)) {
3041
+ if (current) {
3042
+ tokens.push(current);
3043
+ current = "";
3044
+ }
3045
+ continue;
3046
+ }
3047
+ if (/[;&|<>`$(){}\n\r]/.test(char)) return null;
3048
+ current += char;
3049
+ }
3050
+
3051
+ if (escaping || quote) return null;
3052
+ if (current) tokens.push(current);
3053
+ return tokens;
3054
+ }
3055
+
3056
+ function findLiteralBdExecutableIndex(tokens: string[]): number {
3057
+ if (tokens[0] === "bd") return 0;
3058
+ if (tokens[0] === "command" || tokens[0] === "builtin" || tokens[0] === "exec" || tokens[0] === "nohup") {
3059
+ return tokens[1] === "bd" ? 1 : -1;
3060
+ }
3061
+ if (tokens[0] !== "env") return -1;
3062
+
3063
+ for (let index = 1; index < tokens.length; index += 1) {
3064
+ const token = tokens[index] ?? "";
3065
+ if (token === "bd") return index;
3066
+ if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
3067
+ if (token.startsWith("-")) continue;
3068
+ return -1;
3069
+ }
3070
+ return -1;
3071
+ }
3072
+
3073
+ function isAllowedRalplanBeadsDbPath(cwd: string, rawPath: string): boolean {
3074
+ const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
3075
+ if (!trimmed || trimmed.includes("\0")) return false;
3076
+ let relativePath: string;
3077
+ try {
3078
+ const absolute = resolve(cwd, trimmed);
3079
+ relativePath = relative(cwd, absolute).replace(/\\/g, "/");
3080
+ } catch {
3081
+ return false;
3082
+ }
3083
+ return relativePath.startsWith(".beads/") && relativePath.length > ".beads/".length;
3084
+ }
3085
+
3086
+ function classifyRalplanBeadsMetadataCommand(cwd: string, command: string): RalplanBeadsCommandClassification {
3087
+ const trimmedCommand = command.trim();
3088
+ const startsWithBd = /^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"$`]*"|'[^']*'|[^\s"'$`;&|<>]+)\s+)*bd(?:\s|$)/.test(trimmedCommand);
3089
+ const hasCompoundBd = /[;&|()]\s*bd(?:\s|$)/.test(command);
3090
+ const tokens = shellTokenizeLiteralCommand(command);
3091
+ const bdExecutableIndex = tokens ? findLiteralBdExecutableIndex(tokens) : -1;
3092
+ if (!startsWithBd && !hasCompoundBd && bdExecutableIndex === -1) return { present: false, allowed: false };
3093
+
3094
+ if (!tokens || bdExecutableIndex !== 0) {
3095
+ return { present: true, allowed: false, reason: "Beads tracker command must be a single literal bd invocation" };
3096
+ }
3097
+
3098
+ let dbPath = "";
3099
+ let dbValueIndex = -1;
3100
+ for (let index = 1; index < tokens.length; index += 1) {
3101
+ const token = tokens[index] ?? "";
3102
+ if (token === "--db") {
3103
+ dbPath = tokens[index + 1] ?? "";
3104
+ dbValueIndex = index + 1;
3105
+ break;
3106
+ }
3107
+ if (token.startsWith("--db=")) {
3108
+ dbPath = token.slice("--db=".length);
3109
+ dbValueIndex = index;
3110
+ break;
3111
+ }
3112
+ }
3113
+
3114
+ if (!dbPath) {
3115
+ return { present: true, allowed: false, reason: "Beads tracker command is missing a literal --db .beads/<db> target" };
3116
+ }
3117
+ if (!isAllowedRalplanBeadsDbPath(cwd, dbPath)) {
3118
+ return { present: true, allowed: false, reason: `Beads tracker db target ${dbPath} is outside repo-local .beads metadata` };
3119
+ }
3120
+
3121
+ const operationTokens = tokens
3122
+ .slice(dbValueIndex + 1)
3123
+ .filter((token) => token && !token.startsWith("-"));
3124
+ const operation = operationTokens[0] ?? "";
3125
+ const suboperation = operationTokens[1] ?? "";
3126
+ if (["create", "update", "edit", "close", "reopen", "status", "dep"].includes(operation)) {
3127
+ return { present: true, allowed: true };
3128
+ }
3129
+ if (operation === "comments" && suboperation === "add") {
3130
+ return { present: true, allowed: true };
3131
+ }
3132
+ return {
3133
+ present: true,
3134
+ allowed: false,
3135
+ reason: operation
3136
+ ? `Beads tracker operation ${operation}${suboperation ? ` ${suboperation}` : ""} is not allowed during planning`
3137
+ : "Beads tracker command is missing an allowed metadata operation",
3138
+ };
3139
+ }
3140
+
2694
3141
  function readPreToolUseCommand(payload: CodexHookPayload): string {
2695
3142
  const toolInput = safeObject(payload.tool_input);
2696
3143
  return safeString(toolInput.command).trim();
@@ -2751,6 +3198,31 @@ function isNullDeviceRedirectTarget(target: string): boolean {
2751
3198
  return normalized === "/dev/null" || normalized === "nul";
2752
3199
  }
2753
3200
 
3201
+ // Collects same-command literal variable assignments (`NAME="value"`), skipping
3202
+ // any value that involves expansion (`$`, backticks) so unresolved/dynamic
3203
+ // targets stay conservatively blocked.
3204
+ function extractCommandLiteralAssignments(command: string): Map<string, string> {
3205
+ const assignments = new Map<string, string>();
3206
+ const pattern = /(?:^|[\n;&|(]|&&|\|\|)\s*([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"$`]*)"|'([^']*)'|([^\s"'$`;&|<>]+))/g;
3207
+ for (const match of command.matchAll(pattern)) {
3208
+ const name = safeString(match[1]).trim();
3209
+ if (!name) continue;
3210
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
3211
+ assignments.set(name, value);
3212
+ }
3213
+ return assignments;
3214
+ }
3215
+
3216
+ // Resolves a redirect/tee target of the form `$NAME`/`${NAME}` against
3217
+ // same-command literal assignments; non-variable or unresolved targets are
3218
+ // returned unchanged so they remain subject to the allowed-path check.
3219
+ function resolveCommandRedirectTarget(target: string, assignments: Map<string, string>): string {
3220
+ const variableMatch = target.match(/^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/);
3221
+ if (!variableMatch) return target;
3222
+ const resolved = assignments.get(safeString(variableMatch[1]));
3223
+ return resolved !== undefined ? resolved : target;
3224
+ }
3225
+
2754
3226
  function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
2755
3227
  const targets: string[] = [];
2756
3228
  for (const match of command.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
@@ -2761,7 +3233,7 @@ function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
2761
3233
  }
2762
3234
 
2763
3235
  function commandHasDeepInterviewWriteIntent(command: string): boolean {
2764
- return /\bapply_patch\b/.test(command)
3236
+ return commandInvokesApplyPatch(command)
2765
3237
  || extractDeepInterviewCommandRedirectTargets(command).length > 0
2766
3238
  || /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
2767
3239
  || /\bsed\s+(?:[^\n;&|]*\s)?-i(?:\b|['"])/.test(command)
@@ -2770,13 +3242,42 @@ function commandHasDeepInterviewWriteIntent(command: string): boolean {
2770
3242
  }
2771
3243
 
2772
3244
  function extractDeepInterviewCommandWriteTargets(command: string): string[] {
2773
- const targets = extractDeepInterviewCommandRedirectTargets(command);
3245
+ const assignments = extractCommandLiteralAssignments(command);
3246
+ const targets = extractDeepInterviewCommandRedirectTargets(command)
3247
+ .map((target) => resolveCommandRedirectTarget(target, assignments));
2774
3248
  for (const match of command.matchAll(/\btee\s+(?:-a\s+)?(["']?)([^\s&|;<>]+)\1/g)) {
2775
3249
  const candidate = safeString(match[2]).trim();
2776
- if (candidate) targets.push(candidate);
3250
+ if (candidate) targets.push(resolveCommandRedirectTarget(candidate, assignments));
2777
3251
  }
2778
3252
  return targets;
2779
3253
  }
3254
+ function formatPlanningWriteBlockDetail(
3255
+ operationClass: string,
3256
+ target: string | undefined,
3257
+ allowedPrefixes: readonly string[],
3258
+ ): string {
3259
+ const targetDetail = target ? `target ${target}` : "target <unresolved>";
3260
+ return `${operationClass} ${targetDetail} is not under allowed planning artifact paths (${allowedPrefixes.join(", ")})`;
3261
+ }
3262
+
3263
+ function isUnresolvedVariableTarget(target: string): boolean {
3264
+ const normalized = target.trim().replace(/^['"]|['"]$/g, "");
3265
+ return /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(normalized);
3266
+ }
3267
+
3268
+
3269
+ function describeImplementationToolBlock(
3270
+ toolName: string,
3271
+ blockedPath: string | undefined,
3272
+ pathCount: number,
3273
+ ): string {
3274
+ if (pathCount === 0) {
3275
+ const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target extraction failed" : `${toolName} path`;
3276
+ return `${operationClass} target <unresolved>; only planning artifact paths are allowed (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3277
+ }
3278
+ const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target" : `${toolName} path`;
3279
+ return formatPlanningWriteBlockDetail(operationClass, blockedPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
3280
+ }
2780
3281
 
2781
3282
  function isAllowedDeepInterviewBashWrite(cwd: string, command: string): boolean {
2782
3283
  if (!commandHasDeepInterviewWriteIntent(command)) return true;
@@ -2871,6 +3372,7 @@ async function readActiveRalplanStateForPreToolUse(
2871
3372
  ));
2872
3373
  if (!hasActiveAutopilotSkill) return null;
2873
3374
  const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
3375
+ if (isAutopilotReviewReworkPhase(autopilotStatePhase)) return null;
2874
3376
  if (!canAutopilotSkillMirrorSupplyRalplanPhase(autopilotStatePhase)) return null;
2875
3377
  const hasRalplanScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
2876
3378
  entry.skill === "autopilot"
@@ -2882,10 +3384,37 @@ async function readActiveRalplanStateForPreToolUse(
2882
3384
  }
2883
3385
 
2884
3386
  function isAllowedRalplanBashWrite(cwd: string, command: string): boolean {
3387
+ const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
3388
+ const targets = extractDeepInterviewCommandWriteTargets(command);
3389
+ const hasAllowedTargets = targets.length > 0
3390
+ && targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
3391
+
3392
+ if (beadsCommand.present) {
3393
+ return beadsCommand.allowed && (targets.length === 0 || hasAllowedTargets);
3394
+ }
2885
3395
  if (!commandHasDeepInterviewWriteIntent(command)) return true;
2886
3396
  if (/\bomx\s+(?:state\s+(?:write|read|clear)|question)\b/.test(command)) return true;
3397
+ return hasAllowedTargets;
3398
+ }
3399
+
3400
+ function buildRalplanBashBlockedDetail(cwd: string, command: string): string {
2887
3401
  const targets = extractDeepInterviewCommandWriteTargets(command);
2888
- return targets.length > 0 && targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
3402
+ const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
3403
+ if (blockedTarget && isUnresolvedVariableTarget(blockedTarget)) {
3404
+ return `unresolved Bash write target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3405
+ }
3406
+ if (blockedTarget) {
3407
+ const operationClass = /\btee\s+(?:-a\s+)?/.test(command) ? "Bash tee write" : "Bash redirect write";
3408
+ return `${operationClass} target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3409
+ }
3410
+ const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
3411
+ if (beadsCommand.present && !beadsCommand.allowed) {
3412
+ return beadsCommand.reason ?? "Beads tracker command is not an allowed planning metadata mutation";
3413
+ }
3414
+ if (beadsCommand.present) {
3415
+ return "Beads tracker command also performs an implementation write outside allowed planning metadata";
3416
+ }
3417
+ return "Bash write intent did not identify an allowed planning artifact path or metadata path";
2889
3418
  }
2890
3419
 
2891
3420
  async function buildRalplanPreToolUseBoundaryOutput(
@@ -2908,21 +3437,18 @@ async function buildRalplanPreToolUseBoundaryOutput(
2908
3437
  if (toolName === "Bash") {
2909
3438
  blocked = !isAllowedRalplanBashWrite(cwd, command);
2910
3439
  if (blocked) {
2911
- const targets = extractDeepInterviewCommandWriteTargets(command);
2912
- const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
2913
- blockedDetail = blockedTarget
2914
- ? `write target ${blockedTarget} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`
2915
- : "Bash write intent did not identify an allowed planning artifact path";
3440
+ blockedDetail = buildRalplanBashBlockedDetail(cwd, command);
2916
3441
  }
2917
3442
  } else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
2918
- if (pathCandidates.length === 0) {
3443
+ const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
3444
+ if (toolPathCandidates.length === 0) {
2919
3445
  blocked = true;
2920
- blockedDetail = `${toolName} did not include a file path; only planning artifact paths are allowed`;
3446
+ blockedDetail = describeImplementationToolBlock(toolName, undefined, toolPathCandidates.length);
2921
3447
  } else {
2922
- const blockedPath = pathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
3448
+ const blockedPath = toolPathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
2923
3449
  blocked = blockedPath !== undefined;
2924
3450
  if (blockedPath !== undefined) {
2925
- blockedDetail = `path ${blockedPath} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3451
+ blockedDetail = describeImplementationToolBlock(toolName, blockedPath, toolPathCandidates.length);
2926
3452
  }
2927
3453
  }
2928
3454
  }
@@ -2942,7 +3468,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
2942
3468
  hookEventName: "PreToolUse",
2943
3469
  additionalContext:
2944
3470
  `${planningModeDescription}. `
2945
- + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, or required `.omx/state/` files. "
3471
+ + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
2946
3472
  + "Do not edit implementation files or run implementation-focused writes from planning phases. "
2947
3473
  + `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
2948
3474
  },
@@ -4221,28 +4747,50 @@ export async function dispatchCodexNativeHook(
4221
4747
  if (hookEventName === "SessionStart" && nativeSessionId) {
4222
4748
  const transcriptPath = safeString(payload.transcript_path ?? payload.transcriptPath).trim();
4223
4749
  const subagentSessionStart = readNativeSubagentSessionStartMetadata(transcriptPath);
4224
- if (subagentSessionStart && canonicalSessionId) {
4750
+ if (subagentSessionStart) {
4751
+ // A native child/subagent SessionStart carries a parent_thread_id in its
4752
+ // transcript session_meta. Treat it as a child-agent lifecycle event for
4753
+ // notification suppression and subagent tracking even when the canonical
4754
+ // leader session has not been reconciled yet (#2831). A child start must
4755
+ // never promote itself into a root/leader session or emit an independent
4756
+ // session-start notification at session/minimal verbosity.
4225
4757
  isSubagentSessionStart = true;
4226
- const belongsToCanonicalSession = await nativeSubagentSessionStartBelongsToCanonicalSession(
4227
- cwd,
4228
- canonicalSessionId,
4229
- currentSessionState,
4230
- subagentSessionStart,
4231
- );
4232
- if (belongsToCanonicalSession) {
4233
- resolvedNativeSessionId = nativeSessionId;
4234
- await recordNativeSubagentSessionStart(
4758
+ if (canonicalSessionId) {
4759
+ const belongsToCanonicalSession = await nativeSubagentSessionStartBelongsToCanonicalSession(
4235
4760
  cwd,
4236
4761
  canonicalSessionId,
4237
- nativeSessionId,
4762
+ currentSessionState,
4238
4763
  subagentSessionStart,
4239
- transcriptPath,
4240
4764
  );
4765
+ if (belongsToCanonicalSession) {
4766
+ resolvedNativeSessionId = nativeSessionId;
4767
+ await recordNativeSubagentSessionStart(
4768
+ cwd,
4769
+ canonicalSessionId,
4770
+ nativeSessionId,
4771
+ subagentSessionStart,
4772
+ transcriptPath,
4773
+ );
4774
+ } else {
4775
+ skipCanonicalSessionStartContext = true;
4776
+ resolvedNativeSessionId =
4777
+ safeString(currentSessionState?.native_session_id).trim() || nativeSessionId;
4778
+ await recordIgnoredNativeSubagentSessionStart(
4779
+ cwd,
4780
+ canonicalSessionId,
4781
+ nativeSessionId,
4782
+ subagentSessionStart,
4783
+ transcriptPath,
4784
+ );
4785
+ }
4241
4786
  } else {
4787
+ // No canonical leader session is resolved in this worktree yet. Still
4788
+ // register the child thread under its parent so its later Stop is
4789
+ // recognized as subagent-scoped, skip leader SessionStart context, and
4790
+ // do not reconcile the child as a new root session.
4242
4791
  skipCanonicalSessionStartContext = true;
4243
- resolvedNativeSessionId =
4244
- safeString(currentSessionState?.native_session_id).trim() || nativeSessionId;
4245
- await recordIgnoredNativeSubagentSessionStart(
4792
+ resolvedNativeSessionId = nativeSessionId;
4793
+ await recordNativeSubagentSessionStart(
4246
4794
  cwd,
4247
4795
  canonicalSessionId,
4248
4796
  nativeSessionId,
@@ -4311,7 +4859,8 @@ export async function dispatchCodexNativeHook(
4311
4859
 
4312
4860
  if (hookEventName === "UserPromptSubmit") {
4313
4861
  const prompt = readPromptText(payload);
4314
- goalWorkflowAdditionalContext = await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
4862
+ goalWorkflowAdditionalContext = await buildCompletedGoalCleanupPromptWarning(cwd, prompt).catch(() => null)
4863
+ ?? await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
4315
4864
  ultragoalSteeringAdditionalContext = prompt && !isSubagentPromptSubmit
4316
4865
  ? 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)}`)
4317
4866
  : null;
@@ -4484,8 +5033,10 @@ export async function dispatchCodexNativeHook(
4484
5033
  : "";
4485
5034
  outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
4486
5035
  ?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
5036
+ ?? await buildNativeSubagentCapacityCloseGuardOutput(payload, cwd, stateDir)
4487
5037
  ?? buildNativePreToolUseOutput(payload);
4488
5038
  } else if (hookEventName === "PostToolUse") {
5039
+ await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => {});
4489
5040
  if (detectMcpTransportFailure(payload)) {
4490
5041
  await markTeamTransportFailure(cwd, payload);
4491
5042
  }
@@ -4494,7 +5045,7 @@ export async function dispatchCodexNativeHook(
4494
5045
  } else if (hookEventName === "Stop") {
4495
5046
  outputJson = await buildStopHookOutput(payload, cwd, stateDir, {
4496
5047
  skipRalphStopBlock: isSubagentStop,
4497
- });
5048
+ }) ?? await buildCompletedGoalCleanupStopOutput(payload, cwd);
4498
5049
  }
4499
5050
 
4500
5051
  return {
@@ -4607,6 +5158,9 @@ function buildMalformedStdinHookOutput(
4607
5158
  const systemMessage =
4608
5159
  `${reason} stdin JSON parsing failed inside codex-native-hook: ${parseError.message}.`;
4609
5160
  const inferredHookEventName = inferHookEventNameFromMalformedInput(rawInput);
5161
+ if (inferredHookEventName === "PreToolUse") {
5162
+ return { systemMessage };
5163
+ }
4610
5164
  if (inferredHookEventName === "Stop" || (!inferredHookEventName && hasNativeStopRuntimeSurface(cwd))) {
4611
5165
  return {
4612
5166
  decision: "block",
@@ -4649,11 +5203,14 @@ async function buildOversizedStdinHookOutput(
4649
5203
  rawHookEventName: CodexHookEventName | null,
4650
5204
  cwd: string,
4651
5205
  ): Promise<Record<string, unknown>> {
5206
+ const systemMessage =
5207
+ `OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
5208
+ if (rawHookEventName === "PreToolUse") {
5209
+ return { systemMessage };
5210
+ }
4652
5211
  if (rawHookEventName === "Stop") {
4653
5212
  return await buildOversizedStopActiveWorkflowOutput(cwd) ?? {};
4654
5213
  }
4655
- const systemMessage =
4656
- `OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
4657
5214
  return {
4658
5215
  continue: false,
4659
5216
  stopReason: "native_hook_stdin_oversized",
@@ -4767,8 +5324,8 @@ export async function runCodexNativeHookCli(): Promise<void> {
4767
5324
 
4768
5325
  const result = await dispatchCodexNativeHook(payload);
4769
5326
  if (result.outputJson) {
4770
- writeNativeHookJsonStdout(result.outputJson);
4771
- } else if (result.hookEventName === "Stop") {
5327
+ writeNativeHookJsonStdout(sanitizeCodexHookOutput(result.hookEventName, result.outputJson) ?? {});
5328
+ } else if (result.hookEventName !== "PreCompact" && result.hookEventName !== "PostCompact") {
4772
5329
  writeNativeHookJsonStdout({});
4773
5330
  }
4774
5331
  } catch (error) {