oh-my-codex 0.18.13 → 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 (277) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +31 -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 +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__/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-spark-routing.test.js +13 -0
  42. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
  43. package/dist/cli/__tests__/doctor-warning-copy.test.js +1 -0
  44. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  45. package/dist/cli/__tests__/index.test.js +5 -0
  46. package/dist/cli/__tests__/index.test.js.map +1 -1
  47. package/dist/cli/__tests__/nested-help-routing.test.js +4 -0
  48. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  49. package/dist/cli/__tests__/package-bin-contract.test.js +7 -1
  50. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  51. package/dist/cli/__tests__/performance-goal.test.js +22 -0
  52. package/dist/cli/__tests__/performance-goal.test.js.map +1 -1
  53. package/dist/cli/__tests__/resume.test.js +96 -0
  54. package/dist/cli/__tests__/resume.test.js.map +1 -1
  55. package/dist/cli/__tests__/session-scoped-runtime.test.js +162 -0
  56. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  57. package/dist/cli/__tests__/session-search.test.js +36 -0
  58. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  59. package/dist/cli/__tests__/setup-agents-overwrite.test.js +35 -1
  60. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  61. package/dist/cli/__tests__/setup-install-mode.test.js +33 -0
  62. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  63. package/dist/cli/__tests__/team.test.js +17 -3
  64. package/dist/cli/__tests__/team.test.js.map +1 -1
  65. package/dist/cli/__tests__/ultragoal.test.js +37 -0
  66. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  67. package/dist/cli/__tests__/update.test.js +1 -0
  68. package/dist/cli/__tests__/update.test.js.map +1 -1
  69. package/dist/cli/autoresearch-goal.d.ts.map +1 -1
  70. package/dist/cli/autoresearch-goal.js +2 -1
  71. package/dist/cli/autoresearch-goal.js.map +1 -1
  72. package/dist/cli/doctor.d.ts +22 -0
  73. package/dist/cli/doctor.d.ts.map +1 -1
  74. package/dist/cli/doctor.js +263 -4
  75. package/dist/cli/doctor.js.map +1 -1
  76. package/dist/cli/index.d.ts +1 -1
  77. package/dist/cli/index.d.ts.map +1 -1
  78. package/dist/cli/index.js +65 -4
  79. package/dist/cli/index.js.map +1 -1
  80. package/dist/cli/performance-goal.d.ts.map +1 -1
  81. package/dist/cli/performance-goal.js +4 -2
  82. package/dist/cli/performance-goal.js.map +1 -1
  83. package/dist/cli/plugin-marketplace.d.ts +1 -0
  84. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  85. package/dist/cli/plugin-marketplace.js +67 -5
  86. package/dist/cli/plugin-marketplace.js.map +1 -1
  87. package/dist/cli/project-runtime-codex-homes.d.ts +2 -0
  88. package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -1
  89. package/dist/cli/project-runtime-codex-homes.js +112 -5
  90. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  91. package/dist/cli/setup.d.ts.map +1 -1
  92. package/dist/cli/setup.js +15 -6
  93. package/dist/cli/setup.js.map +1 -1
  94. package/dist/cli/ultragoal.d.ts.map +1 -1
  95. package/dist/cli/ultragoal.js +5 -1
  96. package/dist/cli/ultragoal.js.map +1 -1
  97. package/dist/cli/update.d.ts.map +1 -1
  98. package/dist/cli/update.js +3 -0
  99. package/dist/cli/update.js.map +1 -1
  100. package/dist/config/__tests__/models.test.js +21 -1
  101. package/dist/config/__tests__/models.test.js.map +1 -1
  102. package/dist/config/models.d.ts +5 -0
  103. package/dist/config/models.d.ts.map +1 -1
  104. package/dist/config/models.js +23 -0
  105. package/dist/config/models.js.map +1 -1
  106. package/dist/goal-workflows/codex-goal-snapshot.d.ts +2 -0
  107. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  108. package/dist/goal-workflows/codex-goal-snapshot.js +10 -0
  109. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  110. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +7 -0
  111. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  112. package/dist/hooks/__tests__/keyword-detector.test.js +46 -0
  113. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  114. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +100 -0
  115. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  116. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +75 -0
  117. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  118. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +225 -0
  119. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  120. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +75 -0
  121. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  122. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +200 -0
  123. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  124. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +187 -12
  125. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  126. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +225 -0
  127. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  128. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  129. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  130. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +75 -0
  131. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  132. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  133. package/dist/hooks/keyword-detector.js +4 -0
  134. package/dist/hooks/keyword-detector.js.map +1 -1
  135. package/dist/hud/__tests__/reconcile.test.js +137 -2
  136. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  137. package/dist/hud/__tests__/state.test.js +107 -0
  138. package/dist/hud/__tests__/state.test.js.map +1 -1
  139. package/dist/hud/__tests__/tmux.test.js +17 -0
  140. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  141. package/dist/hud/reconcile.d.ts.map +1 -1
  142. package/dist/hud/reconcile.js +21 -7
  143. package/dist/hud/reconcile.js.map +1 -1
  144. package/dist/hud/render.d.ts.map +1 -1
  145. package/dist/hud/render.js +9 -0
  146. package/dist/hud/render.js.map +1 -1
  147. package/dist/hud/state.d.ts.map +1 -1
  148. package/dist/hud/state.js +43 -5
  149. package/dist/hud/state.js.map +1 -1
  150. package/dist/hud/types.d.ts +6 -0
  151. package/dist/hud/types.d.ts.map +1 -1
  152. package/dist/hud/types.js.map +1 -1
  153. package/dist/modes/__tests__/base-autopilot-gates.test.js +48 -0
  154. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  155. package/dist/modes/base.d.ts.map +1 -1
  156. package/dist/modes/base.js +1 -0
  157. package/dist/modes/base.js.map +1 -1
  158. package/dist/performance-goal/artifacts.d.ts.map +1 -1
  159. package/dist/performance-goal/artifacts.js +10 -1
  160. package/dist/performance-goal/artifacts.js.map +1 -1
  161. package/dist/ralplan/__tests__/consensus-gate.test.js +112 -0
  162. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  163. package/dist/ralplan/consensus-gate.d.ts +23 -0
  164. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  165. package/dist/ralplan/consensus-gate.js +133 -21
  166. package/dist/ralplan/consensus-gate.js.map +1 -1
  167. package/dist/scripts/__tests__/codex-native-hook.test.js +831 -2
  168. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  169. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  170. package/dist/scripts/codex-native-hook.js +486 -21
  171. package/dist/scripts/codex-native-hook.js.map +1 -1
  172. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  173. package/dist/scripts/notify-hook/team-tmux-guard.js +87 -8
  174. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  175. package/dist/scripts/notify-hook/tmux-injection.js +1 -1
  176. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  177. package/dist/scripts/run-compiled-ci.d.ts +2 -0
  178. package/dist/scripts/run-compiled-ci.d.ts.map +1 -0
  179. package/dist/scripts/run-compiled-ci.js +77 -0
  180. package/dist/scripts/run-compiled-ci.js.map +1 -0
  181. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  182. package/dist/scripts/sync-plugin-mirror.js +1 -0
  183. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  184. package/dist/session-history/search.d.ts.map +1 -1
  185. package/dist/session-history/search.js +24 -9
  186. package/dist/session-history/search.js.map +1 -1
  187. package/dist/state/__tests__/operations.test.js +107 -3
  188. package/dist/state/__tests__/operations.test.js.map +1 -1
  189. package/dist/state/operations.d.ts.map +1 -1
  190. package/dist/state/operations.js +57 -2
  191. package/dist/state/operations.js.map +1 -1
  192. package/dist/team/__tests__/delivery-e2e-smoke.test.js +22 -1
  193. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  194. package/dist/team/__tests__/model-contract.test.js +57 -1
  195. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  196. package/dist/team/__tests__/runtime.test.js +1468 -110
  197. package/dist/team/__tests__/runtime.test.js.map +1 -1
  198. package/dist/team/__tests__/scaling.test.js +63 -0
  199. package/dist/team/__tests__/scaling.test.js.map +1 -1
  200. package/dist/team/__tests__/state.test.js +24 -1
  201. package/dist/team/__tests__/state.test.js.map +1 -1
  202. package/dist/team/__tests__/tmux-session.test.js +512 -0
  203. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  204. package/dist/team/__tests__/worker-bootstrap.test.js +42 -9
  205. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  206. package/dist/team/model-contract.d.ts +16 -0
  207. package/dist/team/model-contract.d.ts.map +1 -1
  208. package/dist/team/model-contract.js +41 -1
  209. package/dist/team/model-contract.js.map +1 -1
  210. package/dist/team/runtime.d.ts.map +1 -1
  211. package/dist/team/runtime.js +116 -30
  212. package/dist/team/runtime.js.map +1 -1
  213. package/dist/team/scaling.d.ts.map +1 -1
  214. package/dist/team/scaling.js +9 -1
  215. package/dist/team/scaling.js.map +1 -1
  216. package/dist/team/state/types.d.ts +2 -0
  217. package/dist/team/state/types.d.ts.map +1 -1
  218. package/dist/team/state/types.js.map +1 -1
  219. package/dist/team/state.d.ts +3 -0
  220. package/dist/team/state.d.ts.map +1 -1
  221. package/dist/team/state.js +20 -0
  222. package/dist/team/state.js.map +1 -1
  223. package/dist/team/tmux-session.d.ts +38 -3
  224. package/dist/team/tmux-session.d.ts.map +1 -1
  225. package/dist/team/tmux-session.js +275 -36
  226. package/dist/team/tmux-session.js.map +1 -1
  227. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  228. package/dist/team/worker-bootstrap.js +26 -1
  229. package/dist/team/worker-bootstrap.js.map +1 -1
  230. package/dist/ultragoal/__tests__/artifacts.test.js +238 -0
  231. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  232. package/dist/ultragoal/__tests__/docs-contract.test.js +3 -0
  233. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  234. package/dist/ultragoal/artifacts.d.ts +15 -0
  235. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  236. package/dist/ultragoal/artifacts.js +167 -4
  237. package/dist/ultragoal/artifacts.js.map +1 -1
  238. package/dist/utils/__tests__/agents-md.test.js +39 -1
  239. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  240. package/dist/utils/__tests__/agents-model-table.test.js +28 -0
  241. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  242. package/dist/utils/agents-md.d.ts +4 -0
  243. package/dist/utils/agents-md.d.ts.map +1 -1
  244. package/dist/utils/agents-md.js +25 -0
  245. package/dist/utils/agents-md.js.map +1 -1
  246. package/dist/utils/agents-model-table.d.ts +7 -3
  247. package/dist/utils/agents-model-table.d.ts.map +1 -1
  248. package/dist/utils/agents-model-table.js +16 -8
  249. package/dist/utils/agents-model-table.js.map +1 -1
  250. package/dist/verification/__tests__/ci-rust-gates.test.js +4 -0
  251. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  252. package/package.json +2 -2
  253. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  254. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +59 -8
  255. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +9 -5
  256. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +3 -0
  257. package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +3 -0
  258. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  259. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +22 -4
  260. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +2 -2
  261. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +61 -0
  262. package/skills/autopilot/SKILL.md +9 -5
  263. package/skills/autoresearch-goal/SKILL.md +3 -0
  264. package/skills/ecomode/SKILL.md +1 -1
  265. package/skills/ecomode/references/agent-tiers.md +61 -0
  266. package/skills/performance-goal/SKILL.md +3 -0
  267. package/skills/ralplan/SKILL.md +1 -1
  268. package/skills/ultragoal/SKILL.md +22 -4
  269. package/skills/ultrawork/SKILL.md +2 -2
  270. package/skills/ultrawork/references/agent-tiers.md +61 -0
  271. package/src/scripts/__tests__/codex-native-hook.test.ts +975 -40
  272. package/src/scripts/codex-native-hook.ts +529 -22
  273. package/src/scripts/notify-hook/team-tmux-guard.ts +92 -8
  274. package/src/scripts/notify-hook/tmux-injection.ts +1 -1
  275. package/src/scripts/run-compiled-ci.ts +85 -0
  276. package/src/scripts/sync-plugin-mirror.ts +1 -0
  277. 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,63 @@ 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
+ 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
+
2297
2468
  async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
2298
2469
  const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
2299
2470
  const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
@@ -2545,6 +2716,144 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
2545
2716
  return safeString(payload.turn_id ?? payload.turnId).trim();
2546
2717
  }
2547
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
+
2548
2857
  async function resolveInternalSessionIdForPayload(
2549
2858
  cwd: string,
2550
2859
  payloadSessionId: string,
@@ -2603,6 +2912,7 @@ const RALPLAN_ALLOWED_WRITE_PREFIXES = [
2603
2912
  ".omx/plans",
2604
2913
  ".omx/specs",
2605
2914
  ".omx/state",
2915
+ ".beads",
2606
2916
  ] as const;
2607
2917
 
2608
2918
  const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
@@ -2653,6 +2963,10 @@ function canAutopilotSkillMirrorSupplyRalplanPhase(phase: string): boolean {
2653
2963
  return phase === "" || normalizeAutopilotPhase(phase) === "ralplan";
2654
2964
  }
2655
2965
 
2966
+ function isAutopilotReviewReworkPhase(phase: string): boolean {
2967
+ return normalizeAutopilotPhase(phase) === "rework";
2968
+ }
2969
+
2656
2970
  function hasExplicitExecutionHandoffSkill(
2657
2971
  state: SkillActiveStateLike | null,
2658
2972
  sessionId: string,
@@ -2692,6 +3006,138 @@ function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
2692
3006
  return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
2693
3007
  }
2694
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
+
2695
3141
  function readPreToolUseCommand(payload: CodexHookPayload): string {
2696
3142
  const toolInput = safeObject(payload.tool_input);
2697
3143
  return safeString(toolInput.command).trim();
@@ -2805,6 +3251,33 @@ function extractDeepInterviewCommandWriteTargets(command: string): string[] {
2805
3251
  }
2806
3252
  return targets;
2807
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
+ }
2808
3281
 
2809
3282
  function isAllowedDeepInterviewBashWrite(cwd: string, command: string): boolean {
2810
3283
  if (!commandHasDeepInterviewWriteIntent(command)) return true;
@@ -2899,6 +3372,7 @@ async function readActiveRalplanStateForPreToolUse(
2899
3372
  ));
2900
3373
  if (!hasActiveAutopilotSkill) return null;
2901
3374
  const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
3375
+ if (isAutopilotReviewReworkPhase(autopilotStatePhase)) return null;
2902
3376
  if (!canAutopilotSkillMirrorSupplyRalplanPhase(autopilotStatePhase)) return null;
2903
3377
  const hasRalplanScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
2904
3378
  entry.skill === "autopilot"
@@ -2910,10 +3384,37 @@ async function readActiveRalplanStateForPreToolUse(
2910
3384
  }
2911
3385
 
2912
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
+ }
2913
3395
  if (!commandHasDeepInterviewWriteIntent(command)) return true;
2914
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 {
2915
3401
  const targets = extractDeepInterviewCommandWriteTargets(command);
2916
- 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";
2917
3418
  }
2918
3419
 
2919
3420
  async function buildRalplanPreToolUseBoundaryOutput(
@@ -2936,21 +3437,18 @@ async function buildRalplanPreToolUseBoundaryOutput(
2936
3437
  if (toolName === "Bash") {
2937
3438
  blocked = !isAllowedRalplanBashWrite(cwd, command);
2938
3439
  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";
3440
+ blockedDetail = buildRalplanBashBlockedDetail(cwd, command);
2944
3441
  }
2945
3442
  } else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
2946
- if (pathCandidates.length === 0) {
3443
+ const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
3444
+ if (toolPathCandidates.length === 0) {
2947
3445
  blocked = true;
2948
- blockedDetail = `${toolName} did not include a file path; only planning artifact paths are allowed`;
3446
+ blockedDetail = describeImplementationToolBlock(toolName, undefined, toolPathCandidates.length);
2949
3447
  } else {
2950
- const blockedPath = pathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
3448
+ const blockedPath = toolPathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
2951
3449
  blocked = blockedPath !== undefined;
2952
3450
  if (blockedPath !== undefined) {
2953
- blockedDetail = `path ${blockedPath} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
3451
+ blockedDetail = describeImplementationToolBlock(toolName, blockedPath, toolPathCandidates.length);
2954
3452
  }
2955
3453
  }
2956
3454
  }
@@ -2970,7 +3468,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
2970
3468
  hookEventName: "PreToolUse",
2971
3469
  additionalContext:
2972
3470
  `${planningModeDescription}. `
2973
- + "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/`. "
2974
3472
  + "Do not edit implementation files or run implementation-focused writes from planning phases. "
2975
3473
  + `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
2976
3474
  },
@@ -4361,7 +4859,8 @@ export async function dispatchCodexNativeHook(
4361
4859
 
4362
4860
  if (hookEventName === "UserPromptSubmit") {
4363
4861
  const prompt = readPromptText(payload);
4364
- goalWorkflowAdditionalContext = await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
4862
+ goalWorkflowAdditionalContext = await buildCompletedGoalCleanupPromptWarning(cwd, prompt).catch(() => null)
4863
+ ?? await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
4365
4864
  ultragoalSteeringAdditionalContext = prompt && !isSubagentPromptSubmit
4366
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)}`)
4367
4866
  : null;
@@ -4534,8 +5033,10 @@ export async function dispatchCodexNativeHook(
4534
5033
  : "";
4535
5034
  outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
4536
5035
  ?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
5036
+ ?? await buildNativeSubagentCapacityCloseGuardOutput(payload, cwd, stateDir)
4537
5037
  ?? buildNativePreToolUseOutput(payload);
4538
5038
  } else if (hookEventName === "PostToolUse") {
5039
+ await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => {});
4539
5040
  if (detectMcpTransportFailure(payload)) {
4540
5041
  await markTeamTransportFailure(cwd, payload);
4541
5042
  }
@@ -4544,7 +5045,7 @@ export async function dispatchCodexNativeHook(
4544
5045
  } else if (hookEventName === "Stop") {
4545
5046
  outputJson = await buildStopHookOutput(payload, cwd, stateDir, {
4546
5047
  skipRalphStopBlock: isSubagentStop,
4547
- });
5048
+ }) ?? await buildCompletedGoalCleanupStopOutput(payload, cwd);
4548
5049
  }
4549
5050
 
4550
5051
  return {
@@ -4657,6 +5158,9 @@ function buildMalformedStdinHookOutput(
4657
5158
  const systemMessage =
4658
5159
  `${reason} stdin JSON parsing failed inside codex-native-hook: ${parseError.message}.`;
4659
5160
  const inferredHookEventName = inferHookEventNameFromMalformedInput(rawInput);
5161
+ if (inferredHookEventName === "PreToolUse") {
5162
+ return { systemMessage };
5163
+ }
4660
5164
  if (inferredHookEventName === "Stop" || (!inferredHookEventName && hasNativeStopRuntimeSurface(cwd))) {
4661
5165
  return {
4662
5166
  decision: "block",
@@ -4699,11 +5203,14 @@ async function buildOversizedStdinHookOutput(
4699
5203
  rawHookEventName: CodexHookEventName | null,
4700
5204
  cwd: string,
4701
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
+ }
4702
5211
  if (rawHookEventName === "Stop") {
4703
5212
  return await buildOversizedStopActiveWorkflowOutput(cwd) ?? {};
4704
5213
  }
4705
- const systemMessage =
4706
- `OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
4707
5214
  return {
4708
5215
  continue: false,
4709
5216
  stopReason: "native_hook_stdin_oversized",
@@ -4817,8 +5324,8 @@ export async function runCodexNativeHookCli(): Promise<void> {
4817
5324
 
4818
5325
  const result = await dispatchCodexNativeHook(payload);
4819
5326
  if (result.outputJson) {
4820
- writeNativeHookJsonStdout(result.outputJson);
4821
- } else if (result.hookEventName === "Stop") {
5327
+ writeNativeHookJsonStdout(sanitizeCodexHookOutput(result.hookEventName, result.outputJson) ?? {});
5328
+ } else if (result.hookEventName !== "PreCompact" && result.hookEventName !== "PostCompact") {
4822
5329
  writeNativeHookJsonStdout({});
4823
5330
  }
4824
5331
  } catch (error) {