oh-my-codex 0.18.13 → 0.18.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +41 -18
- package/dist/agents/__tests__/native-config.test.js +62 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +15 -2
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/fsm.test.js +7 -0
- package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
- package/dist/autopilot/__tests__/planner-routing.test.d.ts +2 -0
- package/dist/autopilot/__tests__/planner-routing.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/planner-routing.test.js +74 -0
- package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js +315 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +2 -2
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autopilot/fsm.js +3 -0
- package/dist/autopilot/fsm.js.map +1 -1
- package/dist/autopilot/planner-routing.d.ts +21 -0
- package/dist/autopilot/planner-routing.d.ts.map +1 -0
- package/dist/autopilot/planner-routing.js +59 -0
- package/dist/autopilot/planner-routing.js.map +1 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +30 -0
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/autoresearch/goal.d.ts.map +1 -1
- package/dist/autoresearch/goal.js +10 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js +33 -0
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js +3 -0
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +30 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.js +158 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-explore-deprecated.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-explore-deprecated.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-explore-deprecated.test.js +33 -0
- package/dist/cli/__tests__/doctor-explore-deprecated.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js +13 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +132 -2
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +5 -0
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +40 -0
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +4 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +7 -1
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/performance-goal.test.js +22 -0
- package/dist/cli/__tests__/performance-goal.test.js.map +1 -1
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.d.ts +2 -0
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.d.ts.map +1 -0
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +64 -0
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.js +96 -0
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +162 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js +36 -0
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +70 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +33 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +17 -3
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +37 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +81 -2
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/url.test.d.ts +2 -0
- package/dist/cli/__tests__/url.test.d.ts.map +1 -0
- package/dist/cli/__tests__/url.test.js +82 -0
- package/dist/cli/__tests__/url.test.js.map +1 -0
- package/dist/cli/autoresearch-goal.d.ts.map +1 -1
- package/dist/cli/autoresearch-goal.js +2 -1
- package/dist/cli/autoresearch-goal.js.map +1 -1
- package/dist/cli/autoresearch-intake.d.ts.map +1 -1
- package/dist/cli/autoresearch-intake.js +2 -0
- package/dist/cli/autoresearch-intake.js.map +1 -1
- package/dist/cli/doctor.d.ts +39 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +460 -40
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +71 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/performance-goal.d.ts.map +1 -1
- package/dist/cli/performance-goal.js +4 -2
- package/dist/cli/performance-goal.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +1 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +88 -67
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.d.ts +2 -0
- package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js +112 -5
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +5 -1
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +23 -4
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/url.d.ts +8 -0
- package/dist/cli/url.d.ts.map +1 -0
- package/dist/cli/url.js +69 -0
- package/dist/cli/url.js.map +1 -0
- package/dist/config/__tests__/codex-hooks.test.js +20 -4
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +21 -1
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +1 -4
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/models.d.ts +5 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +23 -0
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +10 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +7 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +46 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +100 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +225 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +563 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +187 -12
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +225 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +4 -0
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +137 -2
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +107 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +17 -0
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +21 -7
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +9 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +43 -5
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/types.d.ts +6 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +24 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +53 -0
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +2 -0
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +32 -6
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +11 -5
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +48 -0
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +1 -0
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/http-client.test.js +9 -0
- package/dist/notifications/__tests__/http-client.test.js.map +1 -1
- package/dist/notifications/http-client.js +1 -1
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/performance-goal/artifacts.d.ts.map +1 -1
- package/dist/performance-goal/artifacts.js +10 -1
- package/dist/performance-goal/artifacts.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +112 -0
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +23 -0
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +133 -21
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1251 -13
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +601 -24
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +1 -0
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +279 -86
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +87 -8
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/run-compiled-ci.d.ts +2 -0
- package/dist/scripts/run-compiled-ci.d.ts.map +1 -0
- package/dist/scripts/run-compiled-ci.js +77 -0
- package/dist/scripts/run-compiled-ci.js.map +1 -0
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +1 -0
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/session-history/search.d.ts.map +1 -1
- package/dist/session-history/search.js +24 -9
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/args.test.d.ts +2 -0
- package/dist/sidecar/__tests__/args.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/args.test.js +18 -0
- package/dist/sidecar/__tests__/args.test.js.map +1 -0
- package/dist/sidecar/index.d.ts.map +1 -1
- package/dist/sidecar/index.js +12 -4
- package/dist/sidecar/index.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +107 -3
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +57 -2
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +22 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +57 -1
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +1468 -110
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +63 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +24 -1
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +512 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +42 -9
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/model-contract.d.ts +16 -0
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +41 -1
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +116 -30
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +9 -1
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/types.d.ts +2 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state.d.ts +3 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +20 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts +38 -3
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +275 -36
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +26 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +388 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +3 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +23 -0
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +275 -12
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/__tests__/url-reader.test.d.ts +2 -0
- package/dist/url-reader/__tests__/url-reader.test.d.ts.map +1 -0
- package/dist/url-reader/__tests__/url-reader.test.js +433 -0
- package/dist/url-reader/__tests__/url-reader.test.js.map +1 -0
- package/dist/url-reader/index.d.ts +4 -0
- package/dist/url-reader/index.d.ts.map +1 -0
- package/dist/url-reader/index.js +522 -0
- package/dist/url-reader/index.js.map +1 -0
- package/dist/url-reader/types.d.ts +41 -0
- package/dist/url-reader/types.d.ts.map +1 -0
- package/dist/url-reader/types.js +2 -0
- package/dist/url-reader/types.js.map +1 -0
- package/dist/utils/__tests__/agents-md.test.js +39 -1
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +28 -0
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts +4 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +28 -0
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts +7 -3
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +16 -8
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/platform-command.d.ts +7 -1
- package/dist/utils/platform-command.d.ts.map +1 -1
- package/dist/utils/platform-command.js +12 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +4 -0
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/vscode/__tests__/index.test.d.ts +2 -0
- package/dist/vscode/__tests__/index.test.d.ts.map +1 -0
- package/dist/vscode/__tests__/index.test.js +236 -0
- package/dist/vscode/__tests__/index.test.js.map +1 -0
- package/dist/vscode/index.d.ts +76 -0
- package/dist/vscode/index.d.ts.map +1 -0
- package/dist/vscode/index.js +196 -0
- package/dist/vscode/index.js.map +1 -0
- package/package.json +3 -3
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +59 -8
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +9 -5
- package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +3 -0
- package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +3 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +22 -4
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +61 -0
- package/skills/autopilot/SKILL.md +9 -5
- package/skills/autoresearch-goal/SKILL.md +3 -0
- package/skills/ecomode/SKILL.md +1 -1
- package/skills/ecomode/references/agent-tiers.md +61 -0
- package/skills/performance-goal/SKILL.md +3 -0
- package/skills/ralplan/SKILL.md +1 -1
- package/skills/ultragoal/SKILL.md +22 -4
- package/skills/ultrawork/SKILL.md +2 -2
- package/skills/ultrawork/references/agent-tiers.md +61 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +1560 -181
- package/src/scripts/codex-native-hook.ts +663 -24
- package/src/scripts/notify-hook/team-leader-nudge.ts +298 -91
- package/src/scripts/notify-hook/team-tmux-guard.ts +92 -8
- package/src/scripts/notify-hook/tmux-injection.ts +1 -1
- package/src/scripts/run-compiled-ci.ts +85 -0
- package/src/scripts/sync-plugin-mirror.ts +1 -0
- 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
|
|
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
|
-
?
|
|
2246
|
-
: `OMX ${mode} is still active (phase: ${phase}).`;
|
|
2357
|
+
? `OMX autopilot is still active (phase: code-review; ${diagnostic}). Run the required $code-review step before completing or clearing Autopilot state.`
|
|
2358
|
+
: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}).`;
|
|
2247
2359
|
return {
|
|
2248
2360
|
decision: "block",
|
|
2249
|
-
reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2361
|
+
reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2250
2362
|
stopReason: `${mode}_${phase}`,
|
|
2251
2363
|
systemMessage,
|
|
2364
|
+
statePath,
|
|
2365
|
+
canonicalDisagreement,
|
|
2252
2366
|
};
|
|
2253
2367
|
}
|
|
2254
2368
|
|
|
@@ -2294,6 +2408,116 @@ function reportsBlockedUltragoalCompletedAggregateMicrogoalLoop(goal: Record<str
|
|
|
2294
2408
|
&& /\b(?:unreconcilable|mismatch|loop|already complete|already completed|blocks?)\b/i.test(evidence);
|
|
2295
2409
|
}
|
|
2296
2410
|
|
|
2411
|
+
|
|
2412
|
+
function sentenceWindowAround(text: string, start: number, end: number): string {
|
|
2413
|
+
const rawBefore = text.slice(Math.max(0, start - 80), start);
|
|
2414
|
+
const rawAfter = text.slice(end, Math.min(text.length, end + 80));
|
|
2415
|
+
const before = rawBefore.slice(Math.max(rawBefore.lastIndexOf("\n"), rawBefore.lastIndexOf("."), rawBefore.lastIndexOf("!"), rawBefore.lastIndexOf("?")) + 1);
|
|
2416
|
+
const sentenceEndOffsets = [rawAfter.indexOf("\n"), rawAfter.indexOf("."), rawAfter.indexOf("!"), rawAfter.indexOf("?")].filter((index) => index >= 0);
|
|
2417
|
+
const after = sentenceEndOffsets.length > 0 ? rawAfter.slice(0, Math.min(...sentenceEndOffsets)) : rawAfter;
|
|
2418
|
+
return `${before}${text.slice(start, end)}${after}`;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function isNegatedGoalAttemptWindow(window: string): boolean {
|
|
2422
|
+
return /\b(?:do\s+not|don't|never|must\s+not|should\s+not|cannot|can't|not|no)\b.{0,50}\b(?:start|create|begin|new|another|goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b/i.test(window)
|
|
2423
|
+
|| /\b(?:without|instead\s+of)\b.{0,30}\b(?:start|create|begin|new|another)?\b.{0,30}\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b/i.test(window)
|
|
2424
|
+
|| /\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b.{0,30}\b(?:is|was|already)\b.{0,30}\b(?:documented|complete|completed|done|unavailable|not\s+needed)\b/i.test(window);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function looksLikeGoalCreationAttempt(text: string): boolean {
|
|
2428
|
+
const candidatePattern = /\b(?:start|create|begin|new|another|goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b/gi;
|
|
2429
|
+
for (const match of text.matchAll(candidatePattern)) {
|
|
2430
|
+
const start = match.index ?? 0;
|
|
2431
|
+
const end = start + match[0].length;
|
|
2432
|
+
const window = sentenceWindowAround(text, start, end);
|
|
2433
|
+
if (isNegatedGoalAttemptWindow(window)) continue;
|
|
2434
|
+
if (/(?:\b(?:start|create|begin|new|another)\b.{0,80}\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b|\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b.{0,80}\b(?:start|create|begin|new|another)\b)/i.test(window)) return true;
|
|
2435
|
+
}
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function looksLikeCreateGoalAttempt(text: string): boolean {
|
|
2440
|
+
const candidatePattern = /\bcreate_goal\s*(?:\(|\b)/gi;
|
|
2441
|
+
for (const match of text.matchAll(candidatePattern)) {
|
|
2442
|
+
const start = match.index ?? 0;
|
|
2443
|
+
const end = start + match[0].length;
|
|
2444
|
+
const window = sentenceWindowAround(text, start, end);
|
|
2445
|
+
if (/(?:\bno\s+create_goal\s+attempt\b|\b(?:do\s+not|don't|never|must\s+not|should\s+not|cannot|can't|not)\b.{0,40}\bcreate_goal\b|\bwithout\s+create_goal\b|\bfailed\s+to\s+call\s+create_goal\b|\bcreate_goal\s+(?:is|was)\s+(?:unavailable|not\s+available|not\s+called)\b)/i.test(window)) {
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
if (/\b(?:call|calling|called|invoke|invoking|start|starting|create|creating|begin|beginning|attempt|attempting|try|trying|continue\s+to|proceed\s+to)\b.{0,80}\bcreate_goal\b/i.test(window)
|
|
2449
|
+
|| /\bcreate_goal\b.{0,80}\b(?:payload|call|tool|now|next|follows|follow|start|starting|create|creating|begin|beginning|attempt|attempting)\b/i.test(window)
|
|
2450
|
+
|| /\bcreate_goal\s*\(/i.test(match[0])) {
|
|
2451
|
+
return true;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function looksLikeCompletedGoalCleanupAttempt(text: string): boolean {
|
|
2458
|
+
return looksLikeGoalCreationAttempt(text) || looksLikeCreateGoalAttempt(text);
|
|
2459
|
+
}
|
|
2460
|
+
function hasFreshNativeGoalCleanupEvidence(text: string): boolean {
|
|
2461
|
+
return /\b(?:get_goal|Codex goal|native goal|active goal|thread goal)\b.{0,120}\b(?:reports?|returns?|shows?|status|state|still|attached|pending|cleanup|required|requires?)\b.{0,120}\b(?:complete|completed|attached|pending|cleanup|required|clear)\b/i.test(text)
|
|
2462
|
+
|| /\b(?:complete|completed|attached|pending|cleanup|required|clear)\b.{0,120}\b(?:get_goal|Codex goal|native goal|active goal|thread goal)\b/i.test(text)
|
|
2463
|
+
|| /\b(?:pending[-_ ]cleanup|native[-_ ]goal[-_ ]cleanup|completed[-_ ]codex[-_ ]goal[-_ ]cleanup)\b/i.test(text);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
async function findCompletedGoalWorkflowCleanupNotice(cwd: string): Promise<string | null> {
|
|
2468
|
+
const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
|
|
2469
|
+
const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
|
|
2470
|
+
const ultragoals = Array.isArray(ultragoal?.goals) ? ultragoal.goals.map(safeObject) : [];
|
|
2471
|
+
if (safeString(aggregateCompletion.status) === "complete" || (ultragoals.length > 0 && ultragoals.every((goal) => safeString(goal.status) === "complete"))) {
|
|
2472
|
+
return buildCodexGoalTerminalCleanupNotice("Ultragoal completion");
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
const performanceRoot = join(cwd, ".omx", "goals", "performance");
|
|
2476
|
+
for (const entry of await readdir(performanceRoot, { withFileTypes: true }).catch(() => [])) {
|
|
2477
|
+
if (!entry.isDirectory()) continue;
|
|
2478
|
+
const state = await readJsonIfExists(join(performanceRoot, entry.name, "state.json"));
|
|
2479
|
+
if (state?.workflow === "performance-goal" && safeString(state.status) === "complete") {
|
|
2480
|
+
return buildCodexGoalTerminalCleanupNotice("Performance-goal completion");
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
const autoresearchRoot = join(cwd, ".omx", "goals", "autoresearch");
|
|
2485
|
+
for (const entry of await readdir(autoresearchRoot, { withFileTypes: true }).catch(() => [])) {
|
|
2486
|
+
if (!entry.isDirectory()) continue;
|
|
2487
|
+
const mission = await readJsonIfExists(join(autoresearchRoot, entry.name, "mission.json"));
|
|
2488
|
+
if (mission?.workflow === "autoresearch-goal" && safeString(mission.status) === "complete") {
|
|
2489
|
+
return buildCodexGoalTerminalCleanupNotice("Autoresearch-goal completion");
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
async function buildCompletedGoalCleanupPromptWarning(cwd: string, prompt: string): Promise<string | null> {
|
|
2497
|
+
if (!looksLikeCompletedGoalCleanupAttempt(prompt)) return null;
|
|
2498
|
+
const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
|
|
2499
|
+
if (!notice) return null;
|
|
2500
|
+
return `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
async function buildCompletedGoalCleanupStopOutput(payload: CodexHookPayload, cwd: string): Promise<Record<string, unknown> | null> {
|
|
2504
|
+
const text = [
|
|
2505
|
+
safeString(payload.last_user_message ?? payload.lastUserMessage),
|
|
2506
|
+
safeString(payload.last_assistant_message ?? payload.lastAssistantMessage),
|
|
2507
|
+
].join("\n");
|
|
2508
|
+
if (!looksLikeCompletedGoalCleanupAttempt(text)) return null;
|
|
2509
|
+
if (!hasFreshNativeGoalCleanupEvidence(text)) return null;
|
|
2510
|
+
const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
|
|
2511
|
+
if (!notice) return null;
|
|
2512
|
+
const systemMessage = `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
|
|
2513
|
+
return {
|
|
2514
|
+
decision: "block",
|
|
2515
|
+
reason: systemMessage,
|
|
2516
|
+
stopReason: "completed_codex_goal_cleanup_required",
|
|
2517
|
+
systemMessage,
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2297
2521
|
async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
|
|
2298
2522
|
const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
|
|
2299
2523
|
const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
|
|
@@ -2545,6 +2769,144 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
|
|
|
2545
2769
|
return safeString(payload.turn_id ?? payload.turnId).trim();
|
|
2546
2770
|
}
|
|
2547
2771
|
|
|
2772
|
+
interface NativeSubagentCapacityBlocker {
|
|
2773
|
+
schema_version: 1;
|
|
2774
|
+
reason: "agent_thread_limit_reached";
|
|
2775
|
+
session_id?: string;
|
|
2776
|
+
thread_id?: string;
|
|
2777
|
+
turn_id?: string;
|
|
2778
|
+
tool_name?: string;
|
|
2779
|
+
error_summary: string;
|
|
2780
|
+
observed_at: string;
|
|
2781
|
+
expires_at: string;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
function nativeSubagentCapacityBlockerPath(stateDir: string): string {
|
|
2785
|
+
return join(stateDir, NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
function stringifyUnknown(value: unknown): string {
|
|
2789
|
+
if (typeof value === "string") return value;
|
|
2790
|
+
if (value === undefined || value === null) return "";
|
|
2791
|
+
try {
|
|
2792
|
+
return JSON.stringify(value);
|
|
2793
|
+
} catch {
|
|
2794
|
+
return String(value);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function payloadEvidenceText(payload: CodexHookPayload): string {
|
|
2799
|
+
return [
|
|
2800
|
+
safeString(payload.tool_name),
|
|
2801
|
+
stringifyUnknown(payload.tool_response),
|
|
2802
|
+
stringifyUnknown(payload.response),
|
|
2803
|
+
stringifyUnknown(payload.error),
|
|
2804
|
+
stringifyUnknown(payload.message),
|
|
2805
|
+
].filter(Boolean).join("\n");
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function isNativeSubagentCapacityFailure(payload: CodexHookPayload): boolean {
|
|
2809
|
+
const evidence = payloadEvidenceText(payload);
|
|
2810
|
+
if (!/\bagent thread limit reached\b/i.test(evidence)) return false;
|
|
2811
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
2812
|
+
return !toolName || /(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function summarizeCapacityFailure(text: string): string {
|
|
2816
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
2817
|
+
if (!normalized) return "agent thread limit reached";
|
|
2818
|
+
const match = normalized.match(/[^.?!\n\r]*agent thread limit reached[^.?!\n\r]*/i);
|
|
2819
|
+
return (match?.[0] ?? normalized).slice(0, 500);
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
async function recordNativeSubagentCapacityBlocker(
|
|
2823
|
+
cwd: string,
|
|
2824
|
+
stateDir: string,
|
|
2825
|
+
payload: CodexHookPayload,
|
|
2826
|
+
): Promise<void> {
|
|
2827
|
+
if (!isNativeSubagentCapacityFailure(payload)) return;
|
|
2828
|
+
const nowMs = Date.now();
|
|
2829
|
+
const blocker: NativeSubagentCapacityBlocker = {
|
|
2830
|
+
schema_version: 1,
|
|
2831
|
+
reason: "agent_thread_limit_reached",
|
|
2832
|
+
...(readPayloadSessionId(payload) ? { session_id: readPayloadSessionId(payload) } : {}),
|
|
2833
|
+
...(readPayloadThreadId(payload) ? { thread_id: readPayloadThreadId(payload) } : {}),
|
|
2834
|
+
...(readPayloadTurnId(payload) ? { turn_id: readPayloadTurnId(payload) } : {}),
|
|
2835
|
+
...(safeString(payload.tool_name).trim() ? { tool_name: safeString(payload.tool_name).trim() } : {}),
|
|
2836
|
+
error_summary: summarizeCapacityFailure(payloadEvidenceText(payload)),
|
|
2837
|
+
observed_at: new Date(nowMs).toISOString(),
|
|
2838
|
+
expires_at: new Date(nowMs + NATIVE_SUBAGENT_CAPACITY_BLOCKER_TTL_MS).toISOString(),
|
|
2839
|
+
};
|
|
2840
|
+
await mkdir(stateDir, { recursive: true });
|
|
2841
|
+
await writeFile(nativeSubagentCapacityBlockerPath(stateDir), JSON.stringify({
|
|
2842
|
+
...blocker,
|
|
2843
|
+
cwd,
|
|
2844
|
+
}, null, 2));
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
function isFreshNativeSubagentCapacityBlocker(
|
|
2848
|
+
blocker: Record<string, unknown> | null,
|
|
2849
|
+
cwd: string,
|
|
2850
|
+
payload: CodexHookPayload,
|
|
2851
|
+
nowMs = Date.now(),
|
|
2852
|
+
): blocker is NativeSubagentCapacityBlocker & Record<string, unknown> {
|
|
2853
|
+
if (!blocker) return false;
|
|
2854
|
+
if (safeString(blocker.reason) !== "agent_thread_limit_reached") return false;
|
|
2855
|
+
const expiresAtMs = Date.parse(safeString(blocker.expires_at));
|
|
2856
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) return false;
|
|
2857
|
+
const blockerCwd = safeString(blocker.cwd).trim();
|
|
2858
|
+
if (blockerCwd) {
|
|
2859
|
+
try {
|
|
2860
|
+
if (resolve(blockerCwd) !== resolve(cwd)) return false;
|
|
2861
|
+
} catch {
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const blockerSessionId = safeString(blocker.session_id).trim();
|
|
2866
|
+
const payloadSessionId = readPayloadSessionId(payload);
|
|
2867
|
+
return !blockerSessionId || !payloadSessionId || blockerSessionId === payloadSessionId;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
function inputContainsCloseAgentRequest(value: unknown): boolean {
|
|
2871
|
+
if (typeof value === "string") return /\bclose_agent\b/i.test(value);
|
|
2872
|
+
if (!value || typeof value !== "object") return false;
|
|
2873
|
+
try {
|
|
2874
|
+
return /\bclose_agent\b/i.test(JSON.stringify(value));
|
|
2875
|
+
} catch {
|
|
2876
|
+
return false;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
function isCloseAgentToolUse(payload: CodexHookPayload): boolean {
|
|
2881
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
2882
|
+
if (/\bclose_agent\b/i.test(toolName)) return true;
|
|
2883
|
+
if (/multi_tool_use\.parallel/i.test(toolName) && inputContainsCloseAgentRequest(payload.tool_input)) return true;
|
|
2884
|
+
return inputContainsCloseAgentRequest(payload.tool_input) && /multi_agent|agent|tool_use/i.test(toolName);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
async function buildNativeSubagentCapacityCloseGuardOutput(
|
|
2888
|
+
payload: CodexHookPayload,
|
|
2889
|
+
cwd: string,
|
|
2890
|
+
stateDir: string,
|
|
2891
|
+
): Promise<Record<string, unknown> | null> {
|
|
2892
|
+
if (!isCloseAgentToolUse(payload)) return null;
|
|
2893
|
+
const blocker = await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir));
|
|
2894
|
+
if (!isFreshNativeSubagentCapacityBlocker(blocker, cwd, payload)) return null;
|
|
2895
|
+
|
|
2896
|
+
const evidence = safeString(blocker.error_summary).trim() || "agent thread limit reached";
|
|
2897
|
+
return {
|
|
2898
|
+
decision: "block",
|
|
2899
|
+
reason: "Native subagent capacity was exhausted recently; model-level close_agent cleanup is blocked because close_agent can hang indefinitely on stale handles.",
|
|
2900
|
+
hookSpecificOutput: {
|
|
2901
|
+
hookEventName: "PreToolUse",
|
|
2902
|
+
additionalContext:
|
|
2903
|
+
`OMX blocked ${safeString(payload.tool_name).trim() || "close_agent"} before it could start: a recent native subagent capacity failure was recorded (${evidence}). `
|
|
2904
|
+
+ "Do not call multi_agent_v1.close_agent, and do not batch close_agent through multi_tool_use.parallel, as stale native handles can hang the whole turn. "
|
|
2905
|
+
+ "Treat this as a bounded capacity blocker: persist/report the blocker evidence, avoid further native subagent cleanup from the model turn, and recover via runtime-level cleanup or a fresh Codex session.",
|
|
2906
|
+
},
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2548
2910
|
async function resolveInternalSessionIdForPayload(
|
|
2549
2911
|
cwd: string,
|
|
2550
2912
|
payloadSessionId: string,
|
|
@@ -2557,9 +2919,11 @@ async function resolveInternalSessionIdForPayload(
|
|
|
2557
2919
|
if (!canonicalSessionId) return payloadSessionId;
|
|
2558
2920
|
|
|
2559
2921
|
const nativeSessionId = safeString(currentSession?.native_session_id).trim();
|
|
2922
|
+
const ownerOmxSessionId = safeString(currentSession?.owner_omx_session_id).trim();
|
|
2560
2923
|
if (!payloadSessionId) return canonicalSessionId;
|
|
2561
2924
|
if (payloadSessionId === canonicalSessionId) return canonicalSessionId;
|
|
2562
2925
|
if (nativeSessionId && payloadSessionId === nativeSessionId) return canonicalSessionId;
|
|
2926
|
+
if (ownerOmxSessionId && payloadSessionId === ownerOmxSessionId) return canonicalSessionId;
|
|
2563
2927
|
return payloadSessionId;
|
|
2564
2928
|
}
|
|
2565
2929
|
|
|
@@ -2603,6 +2967,7 @@ const RALPLAN_ALLOWED_WRITE_PREFIXES = [
|
|
|
2603
2967
|
".omx/plans",
|
|
2604
2968
|
".omx/specs",
|
|
2605
2969
|
".omx/state",
|
|
2970
|
+
".beads",
|
|
2606
2971
|
] as const;
|
|
2607
2972
|
|
|
2608
2973
|
const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
|
|
@@ -2653,6 +3018,10 @@ function canAutopilotSkillMirrorSupplyRalplanPhase(phase: string): boolean {
|
|
|
2653
3018
|
return phase === "" || normalizeAutopilotPhase(phase) === "ralplan";
|
|
2654
3019
|
}
|
|
2655
3020
|
|
|
3021
|
+
function isAutopilotReviewReworkPhase(phase: string): boolean {
|
|
3022
|
+
return normalizeAutopilotPhase(phase) === "rework";
|
|
3023
|
+
}
|
|
3024
|
+
|
|
2656
3025
|
function hasExplicitExecutionHandoffSkill(
|
|
2657
3026
|
state: SkillActiveStateLike | null,
|
|
2658
3027
|
sessionId: string,
|
|
@@ -2692,6 +3061,138 @@ function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
|
|
|
2692
3061
|
return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
2693
3062
|
}
|
|
2694
3063
|
|
|
3064
|
+
interface RalplanBeadsCommandClassification {
|
|
3065
|
+
present: boolean;
|
|
3066
|
+
allowed: boolean;
|
|
3067
|
+
reason?: string;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
function shellTokenizeLiteralCommand(command: string): string[] | null {
|
|
3071
|
+
const tokens: string[] = [];
|
|
3072
|
+
let current = "";
|
|
3073
|
+
let quote: "'" | '"' | null = null;
|
|
3074
|
+
let escaping = false;
|
|
3075
|
+
|
|
3076
|
+
for (const char of command.trim()) {
|
|
3077
|
+
if (escaping) {
|
|
3078
|
+
current += char;
|
|
3079
|
+
escaping = false;
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
if (quote === '"' && char === "\\") {
|
|
3083
|
+
escaping = true;
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
if (quote) {
|
|
3087
|
+
if (char === quote) quote = null;
|
|
3088
|
+
else current += char;
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
if (char === "'" || char === '"') {
|
|
3092
|
+
quote = char;
|
|
3093
|
+
continue;
|
|
3094
|
+
}
|
|
3095
|
+
if (/\s/.test(char)) {
|
|
3096
|
+
if (current) {
|
|
3097
|
+
tokens.push(current);
|
|
3098
|
+
current = "";
|
|
3099
|
+
}
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (/[;&|<>`$(){}\n\r]/.test(char)) return null;
|
|
3103
|
+
current += char;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if (escaping || quote) return null;
|
|
3107
|
+
if (current) tokens.push(current);
|
|
3108
|
+
return tokens;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
function findLiteralBdExecutableIndex(tokens: string[]): number {
|
|
3112
|
+
if (tokens[0] === "bd") return 0;
|
|
3113
|
+
if (tokens[0] === "command" || tokens[0] === "builtin" || tokens[0] === "exec" || tokens[0] === "nohup") {
|
|
3114
|
+
return tokens[1] === "bd" ? 1 : -1;
|
|
3115
|
+
}
|
|
3116
|
+
if (tokens[0] !== "env") return -1;
|
|
3117
|
+
|
|
3118
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
3119
|
+
const token = tokens[index] ?? "";
|
|
3120
|
+
if (token === "bd") return index;
|
|
3121
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
|
|
3122
|
+
if (token.startsWith("-")) continue;
|
|
3123
|
+
return -1;
|
|
3124
|
+
}
|
|
3125
|
+
return -1;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
function isAllowedRalplanBeadsDbPath(cwd: string, rawPath: string): boolean {
|
|
3129
|
+
const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
|
|
3130
|
+
if (!trimmed || trimmed.includes("\0")) return false;
|
|
3131
|
+
let relativePath: string;
|
|
3132
|
+
try {
|
|
3133
|
+
const absolute = resolve(cwd, trimmed);
|
|
3134
|
+
relativePath = relative(cwd, absolute).replace(/\\/g, "/");
|
|
3135
|
+
} catch {
|
|
3136
|
+
return false;
|
|
3137
|
+
}
|
|
3138
|
+
return relativePath.startsWith(".beads/") && relativePath.length > ".beads/".length;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
function classifyRalplanBeadsMetadataCommand(cwd: string, command: string): RalplanBeadsCommandClassification {
|
|
3142
|
+
const trimmedCommand = command.trim();
|
|
3143
|
+
const startsWithBd = /^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"$`]*"|'[^']*'|[^\s"'$`;&|<>]+)\s+)*bd(?:\s|$)/.test(trimmedCommand);
|
|
3144
|
+
const hasCompoundBd = /[;&|()]\s*bd(?:\s|$)/.test(command);
|
|
3145
|
+
const tokens = shellTokenizeLiteralCommand(command);
|
|
3146
|
+
const bdExecutableIndex = tokens ? findLiteralBdExecutableIndex(tokens) : -1;
|
|
3147
|
+
if (!startsWithBd && !hasCompoundBd && bdExecutableIndex === -1) return { present: false, allowed: false };
|
|
3148
|
+
|
|
3149
|
+
if (!tokens || bdExecutableIndex !== 0) {
|
|
3150
|
+
return { present: true, allowed: false, reason: "Beads tracker command must be a single literal bd invocation" };
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
let dbPath = "";
|
|
3154
|
+
let dbValueIndex = -1;
|
|
3155
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
3156
|
+
const token = tokens[index] ?? "";
|
|
3157
|
+
if (token === "--db") {
|
|
3158
|
+
dbPath = tokens[index + 1] ?? "";
|
|
3159
|
+
dbValueIndex = index + 1;
|
|
3160
|
+
break;
|
|
3161
|
+
}
|
|
3162
|
+
if (token.startsWith("--db=")) {
|
|
3163
|
+
dbPath = token.slice("--db=".length);
|
|
3164
|
+
dbValueIndex = index;
|
|
3165
|
+
break;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
if (!dbPath) {
|
|
3170
|
+
return { present: true, allowed: false, reason: "Beads tracker command is missing a literal --db .beads/<db> target" };
|
|
3171
|
+
}
|
|
3172
|
+
if (!isAllowedRalplanBeadsDbPath(cwd, dbPath)) {
|
|
3173
|
+
return { present: true, allowed: false, reason: `Beads tracker db target ${dbPath} is outside repo-local .beads metadata` };
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
const operationTokens = tokens
|
|
3177
|
+
.slice(dbValueIndex + 1)
|
|
3178
|
+
.filter((token) => token && !token.startsWith("-"));
|
|
3179
|
+
const operation = operationTokens[0] ?? "";
|
|
3180
|
+
const suboperation = operationTokens[1] ?? "";
|
|
3181
|
+
if (["create", "update", "edit", "close", "reopen", "status", "dep"].includes(operation)) {
|
|
3182
|
+
return { present: true, allowed: true };
|
|
3183
|
+
}
|
|
3184
|
+
if (operation === "comments" && suboperation === "add") {
|
|
3185
|
+
return { present: true, allowed: true };
|
|
3186
|
+
}
|
|
3187
|
+
return {
|
|
3188
|
+
present: true,
|
|
3189
|
+
allowed: false,
|
|
3190
|
+
reason: operation
|
|
3191
|
+
? `Beads tracker operation ${operation}${suboperation ? ` ${suboperation}` : ""} is not allowed during planning`
|
|
3192
|
+
: "Beads tracker command is missing an allowed metadata operation",
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
|
|
2695
3196
|
function readPreToolUseCommand(payload: CodexHookPayload): string {
|
|
2696
3197
|
const toolInput = safeObject(payload.tool_input);
|
|
2697
3198
|
return safeString(toolInput.command).trim();
|
|
@@ -2805,6 +3306,33 @@ function extractDeepInterviewCommandWriteTargets(command: string): string[] {
|
|
|
2805
3306
|
}
|
|
2806
3307
|
return targets;
|
|
2807
3308
|
}
|
|
3309
|
+
function formatPlanningWriteBlockDetail(
|
|
3310
|
+
operationClass: string,
|
|
3311
|
+
target: string | undefined,
|
|
3312
|
+
allowedPrefixes: readonly string[],
|
|
3313
|
+
): string {
|
|
3314
|
+
const targetDetail = target ? `target ${target}` : "target <unresolved>";
|
|
3315
|
+
return `${operationClass} ${targetDetail} is not under allowed planning artifact paths (${allowedPrefixes.join(", ")})`;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
function isUnresolvedVariableTarget(target: string): boolean {
|
|
3319
|
+
const normalized = target.trim().replace(/^['"]|['"]$/g, "");
|
|
3320
|
+
return /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(normalized);
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
|
|
3324
|
+
function describeImplementationToolBlock(
|
|
3325
|
+
toolName: string,
|
|
3326
|
+
blockedPath: string | undefined,
|
|
3327
|
+
pathCount: number,
|
|
3328
|
+
): string {
|
|
3329
|
+
if (pathCount === 0) {
|
|
3330
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target extraction failed" : `${toolName} path`;
|
|
3331
|
+
return `${operationClass} target <unresolved>; only planning artifact paths are allowed (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3332
|
+
}
|
|
3333
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target" : `${toolName} path`;
|
|
3334
|
+
return formatPlanningWriteBlockDetail(operationClass, blockedPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
3335
|
+
}
|
|
2808
3336
|
|
|
2809
3337
|
function isAllowedDeepInterviewBashWrite(cwd: string, command: string): boolean {
|
|
2810
3338
|
if (!commandHasDeepInterviewWriteIntent(command)) return true;
|
|
@@ -2899,6 +3427,7 @@ async function readActiveRalplanStateForPreToolUse(
|
|
|
2899
3427
|
));
|
|
2900
3428
|
if (!hasActiveAutopilotSkill) return null;
|
|
2901
3429
|
const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
|
|
3430
|
+
if (isAutopilotReviewReworkPhase(autopilotStatePhase)) return null;
|
|
2902
3431
|
if (!canAutopilotSkillMirrorSupplyRalplanPhase(autopilotStatePhase)) return null;
|
|
2903
3432
|
const hasRalplanScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
|
|
2904
3433
|
entry.skill === "autopilot"
|
|
@@ -2910,10 +3439,37 @@ async function readActiveRalplanStateForPreToolUse(
|
|
|
2910
3439
|
}
|
|
2911
3440
|
|
|
2912
3441
|
function isAllowedRalplanBashWrite(cwd: string, command: string): boolean {
|
|
3442
|
+
const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
|
|
3443
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
3444
|
+
const hasAllowedTargets = targets.length > 0
|
|
3445
|
+
&& targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
|
|
3446
|
+
|
|
3447
|
+
if (beadsCommand.present) {
|
|
3448
|
+
return beadsCommand.allowed && (targets.length === 0 || hasAllowedTargets);
|
|
3449
|
+
}
|
|
2913
3450
|
if (!commandHasDeepInterviewWriteIntent(command)) return true;
|
|
2914
3451
|
if (/\bomx\s+(?:state\s+(?:write|read|clear)|question)\b/.test(command)) return true;
|
|
3452
|
+
return hasAllowedTargets;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
function buildRalplanBashBlockedDetail(cwd: string, command: string): string {
|
|
2915
3456
|
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
2916
|
-
|
|
3457
|
+
const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
|
|
3458
|
+
if (blockedTarget && isUnresolvedVariableTarget(blockedTarget)) {
|
|
3459
|
+
return `unresolved Bash write target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3460
|
+
}
|
|
3461
|
+
if (blockedTarget) {
|
|
3462
|
+
const operationClass = /\btee\s+(?:-a\s+)?/.test(command) ? "Bash tee write" : "Bash redirect write";
|
|
3463
|
+
return `${operationClass} target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3464
|
+
}
|
|
3465
|
+
const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
|
|
3466
|
+
if (beadsCommand.present && !beadsCommand.allowed) {
|
|
3467
|
+
return beadsCommand.reason ?? "Beads tracker command is not an allowed planning metadata mutation";
|
|
3468
|
+
}
|
|
3469
|
+
if (beadsCommand.present) {
|
|
3470
|
+
return "Beads tracker command also performs an implementation write outside allowed planning metadata";
|
|
3471
|
+
}
|
|
3472
|
+
return "Bash write intent did not identify an allowed planning artifact path or metadata path";
|
|
2917
3473
|
}
|
|
2918
3474
|
|
|
2919
3475
|
async function buildRalplanPreToolUseBoundaryOutput(
|
|
@@ -2936,21 +3492,18 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
2936
3492
|
if (toolName === "Bash") {
|
|
2937
3493
|
blocked = !isAllowedRalplanBashWrite(cwd, command);
|
|
2938
3494
|
if (blocked) {
|
|
2939
|
-
|
|
2940
|
-
const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
|
|
2941
|
-
blockedDetail = blockedTarget
|
|
2942
|
-
? `write target ${blockedTarget} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`
|
|
2943
|
-
: "Bash write intent did not identify an allowed planning artifact path";
|
|
3495
|
+
blockedDetail = buildRalplanBashBlockedDetail(cwd, command);
|
|
2944
3496
|
}
|
|
2945
3497
|
} else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
2946
|
-
|
|
3498
|
+
const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
|
|
3499
|
+
if (toolPathCandidates.length === 0) {
|
|
2947
3500
|
blocked = true;
|
|
2948
|
-
blockedDetail =
|
|
3501
|
+
blockedDetail = describeImplementationToolBlock(toolName, undefined, toolPathCandidates.length);
|
|
2949
3502
|
} else {
|
|
2950
|
-
const blockedPath =
|
|
3503
|
+
const blockedPath = toolPathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
|
|
2951
3504
|
blocked = blockedPath !== undefined;
|
|
2952
3505
|
if (blockedPath !== undefined) {
|
|
2953
|
-
blockedDetail =
|
|
3506
|
+
blockedDetail = describeImplementationToolBlock(toolName, blockedPath, toolPathCandidates.length);
|
|
2954
3507
|
}
|
|
2955
3508
|
}
|
|
2956
3509
|
}
|
|
@@ -2970,7 +3523,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
2970
3523
|
hookEventName: "PreToolUse",
|
|
2971
3524
|
additionalContext:
|
|
2972
3525
|
`${planningModeDescription}. `
|
|
2973
|
-
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`,
|
|
3526
|
+
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
|
|
2974
3527
|
+ "Do not edit implementation files or run implementation-focused writes from planning phases. "
|
|
2975
3528
|
+ `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
|
|
2976
3529
|
},
|
|
@@ -3060,6 +3613,22 @@ function modeStateMatchesSkillStopContext(
|
|
|
3060
3613
|
return true;
|
|
3061
3614
|
}
|
|
3062
3615
|
|
|
3616
|
+
function modeStateHasExplicitMatchingCwd(state: Record<string, unknown>, cwd: string): boolean {
|
|
3617
|
+
const stateCwd = safeString(
|
|
3618
|
+
state.cwd
|
|
3619
|
+
?? state.workingDirectory
|
|
3620
|
+
?? state.working_directory
|
|
3621
|
+
?? state.project_path,
|
|
3622
|
+
).trim();
|
|
3623
|
+
if (!stateCwd) return false;
|
|
3624
|
+
|
|
3625
|
+
try {
|
|
3626
|
+
return resolve(stateCwd) === resolve(cwd);
|
|
3627
|
+
} catch {
|
|
3628
|
+
return false;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3063
3632
|
async function readBlockingSkillForStop(
|
|
3064
3633
|
cwd: string,
|
|
3065
3634
|
stateDir: string,
|
|
@@ -3178,6 +3747,54 @@ function rootModeStateIsCanonicalForStopContext(
|
|
|
3178
3747
|
return true;
|
|
3179
3748
|
}
|
|
3180
3749
|
|
|
3750
|
+
function hasExplicitSessionScope(state: Record<string, unknown>): boolean {
|
|
3751
|
+
return safeString(
|
|
3752
|
+
state.owner_omx_session_id
|
|
3753
|
+
?? state.session_id
|
|
3754
|
+
?? state.codex_session_id
|
|
3755
|
+
?? state.owner_codex_session_id,
|
|
3756
|
+
).trim() !== "";
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
async function readStateTimestampMs(state: Record<string, unknown>, path: string): Promise<number | null> {
|
|
3760
|
+
return parseTimestampMs(state.updated_at)
|
|
3761
|
+
?? parseTimestampMs(state.completed_at)
|
|
3762
|
+
?? parseTimestampMs(state.created_at)
|
|
3763
|
+
?? await stat(path).then((info) => info.mtimeMs, () => null);
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
async function unscopedRootRalplanStateIsNewerTerminalPlanningCompletion(
|
|
3767
|
+
rootState: Record<string, unknown>,
|
|
3768
|
+
rootPath: string,
|
|
3769
|
+
sessionPath: string,
|
|
3770
|
+
cwd: string,
|
|
3771
|
+
threadId: string,
|
|
3772
|
+
): Promise<boolean> {
|
|
3773
|
+
if (hasExplicitSessionScope(rootState)) return false;
|
|
3774
|
+
|
|
3775
|
+
const stateThreadId = safeString(rootState.owner_codex_thread_id ?? rootState.thread_id).trim();
|
|
3776
|
+
if (threadId && stateThreadId && stateThreadId !== threadId) return false;
|
|
3777
|
+
|
|
3778
|
+
if (!modeStateHasExplicitMatchingCwd(rootState, cwd)) return false;
|
|
3779
|
+
|
|
3780
|
+
const phase = safeString(rootState.current_phase ?? rootState.currentPhase).trim().toLowerCase();
|
|
3781
|
+
if (phase !== "complete" && phase !== "completed") return false;
|
|
3782
|
+
|
|
3783
|
+
const planningComplete = rootState.planning_complete === true || rootState.planningComplete === true;
|
|
3784
|
+
const latestPlanPath = safeString(rootState.latest_plan_path ?? rootState.latestPlanPath).trim();
|
|
3785
|
+
if (!planningComplete || !latestPlanPath) return false;
|
|
3786
|
+
|
|
3787
|
+
const sessionState = await readJsonIfExists(sessionPath);
|
|
3788
|
+
if (!sessionState) return false;
|
|
3789
|
+
const sessionPhase = safeString(sessionState.current_phase ?? sessionState.currentPhase).trim().toLowerCase();
|
|
3790
|
+
if (sessionPhase && TERMINAL_MODE_PHASES.has(sessionPhase)) return false;
|
|
3791
|
+
|
|
3792
|
+
const rootTimestamp = await readStateTimestampMs(rootState, rootPath);
|
|
3793
|
+
const sessionTimestamp = await readStateTimestampMs(sessionState, sessionPath);
|
|
3794
|
+
if (rootTimestamp === null || sessionTimestamp === null) return false;
|
|
3795
|
+
return rootTimestamp > sessionTimestamp;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3181
3798
|
async function shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
|
|
3182
3799
|
cwd: string,
|
|
3183
3800
|
stateDir: string,
|
|
@@ -3185,11 +3802,24 @@ async function shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
|
|
|
3185
3802
|
sessionId: string,
|
|
3186
3803
|
threadId: string,
|
|
3187
3804
|
): Promise<boolean> {
|
|
3188
|
-
const
|
|
3805
|
+
const rootModeStatePath = join(stateDir, `${skill}-state.json`);
|
|
3806
|
+
const rootModeState = await readJsonIfExists(rootModeStatePath);
|
|
3189
3807
|
if (!rootModeState) return false;
|
|
3190
|
-
if (!rootModeStateIsCanonicalForStopContext(rootModeState, cwd, sessionId, threadId)) return false;
|
|
3191
3808
|
if (!isTerminalOrInactiveModeState(rootModeState)) return false;
|
|
3192
3809
|
|
|
3810
|
+
const canonicalRoot = rootModeStateIsCanonicalForStopContext(rootModeState, cwd, sessionId, threadId)
|
|
3811
|
+
&& (skill !== "ralplan" || modeStateHasExplicitMatchingCwd(rootModeState, cwd));
|
|
3812
|
+
const freshUnscopedRoot = canonicalRoot || skill !== "ralplan"
|
|
3813
|
+
? false
|
|
3814
|
+
: await unscopedRootRalplanStateIsNewerTerminalPlanningCompletion(
|
|
3815
|
+
rootModeState,
|
|
3816
|
+
rootModeStatePath,
|
|
3817
|
+
join(stateDir, "sessions", sessionId, `${skill}-state.json`),
|
|
3818
|
+
cwd,
|
|
3819
|
+
threadId,
|
|
3820
|
+
);
|
|
3821
|
+
if (!canonicalRoot && !freshUnscopedRoot) return false;
|
|
3822
|
+
|
|
3193
3823
|
const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
|
|
3194
3824
|
const rootSkillState = await readSkillActiveState(rootPath);
|
|
3195
3825
|
return rootSkillStateHasNoActiveSkillForStopContext(rootSkillState, skill, sessionId, threadId);
|
|
@@ -4361,7 +4991,8 @@ export async function dispatchCodexNativeHook(
|
|
|
4361
4991
|
|
|
4362
4992
|
if (hookEventName === "UserPromptSubmit") {
|
|
4363
4993
|
const prompt = readPromptText(payload);
|
|
4364
|
-
goalWorkflowAdditionalContext = await
|
|
4994
|
+
goalWorkflowAdditionalContext = await buildCompletedGoalCleanupPromptWarning(cwd, prompt).catch(() => null)
|
|
4995
|
+
?? await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
|
|
4365
4996
|
ultragoalSteeringAdditionalContext = prompt && !isSubagentPromptSubmit
|
|
4366
4997
|
? await applyUserPromptUltragoalSteering(cwd, prompt).catch((error) => `OMX native UserPromptSubmit rejected bounded .omx/ultragoal steering for G002-cli-and-prompt-submit-bridge: ${error instanceof Error ? error.message : String(error)}`)
|
|
4367
4998
|
: null;
|
|
@@ -4534,8 +5165,10 @@ export async function dispatchCodexNativeHook(
|
|
|
4534
5165
|
: "";
|
|
4535
5166
|
outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
|
|
4536
5167
|
?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
|
|
5168
|
+
?? await buildNativeSubagentCapacityCloseGuardOutput(payload, cwd, stateDir)
|
|
4537
5169
|
?? buildNativePreToolUseOutput(payload);
|
|
4538
5170
|
} else if (hookEventName === "PostToolUse") {
|
|
5171
|
+
await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => {});
|
|
4539
5172
|
if (detectMcpTransportFailure(payload)) {
|
|
4540
5173
|
await markTeamTransportFailure(cwd, payload);
|
|
4541
5174
|
}
|
|
@@ -4544,7 +5177,7 @@ export async function dispatchCodexNativeHook(
|
|
|
4544
5177
|
} else if (hookEventName === "Stop") {
|
|
4545
5178
|
outputJson = await buildStopHookOutput(payload, cwd, stateDir, {
|
|
4546
5179
|
skipRalphStopBlock: isSubagentStop,
|
|
4547
|
-
});
|
|
5180
|
+
}) ?? await buildCompletedGoalCleanupStopOutput(payload, cwd);
|
|
4548
5181
|
}
|
|
4549
5182
|
|
|
4550
5183
|
return {
|
|
@@ -4657,6 +5290,9 @@ function buildMalformedStdinHookOutput(
|
|
|
4657
5290
|
const systemMessage =
|
|
4658
5291
|
`${reason} stdin JSON parsing failed inside codex-native-hook: ${parseError.message}.`;
|
|
4659
5292
|
const inferredHookEventName = inferHookEventNameFromMalformedInput(rawInput);
|
|
5293
|
+
if (inferredHookEventName === "PreToolUse") {
|
|
5294
|
+
return { systemMessage };
|
|
5295
|
+
}
|
|
4660
5296
|
if (inferredHookEventName === "Stop" || (!inferredHookEventName && hasNativeStopRuntimeSurface(cwd))) {
|
|
4661
5297
|
return {
|
|
4662
5298
|
decision: "block",
|
|
@@ -4699,11 +5335,14 @@ async function buildOversizedStdinHookOutput(
|
|
|
4699
5335
|
rawHookEventName: CodexHookEventName | null,
|
|
4700
5336
|
cwd: string,
|
|
4701
5337
|
): Promise<Record<string, unknown>> {
|
|
5338
|
+
const systemMessage =
|
|
5339
|
+
`OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
|
|
5340
|
+
if (rawHookEventName === "PreToolUse") {
|
|
5341
|
+
return { systemMessage };
|
|
5342
|
+
}
|
|
4702
5343
|
if (rawHookEventName === "Stop") {
|
|
4703
5344
|
return await buildOversizedStopActiveWorkflowOutput(cwd) ?? {};
|
|
4704
5345
|
}
|
|
4705
|
-
const systemMessage =
|
|
4706
|
-
`OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
|
|
4707
5346
|
return {
|
|
4708
5347
|
continue: false,
|
|
4709
5348
|
stopReason: "native_hook_stdin_oversized",
|
|
@@ -4817,8 +5456,8 @@ export async function runCodexNativeHookCli(): Promise<void> {
|
|
|
4817
5456
|
|
|
4818
5457
|
const result = await dispatchCodexNativeHook(payload);
|
|
4819
5458
|
if (result.outputJson) {
|
|
4820
|
-
writeNativeHookJsonStdout(result.outputJson);
|
|
4821
|
-
} else if (result.hookEventName
|
|
5459
|
+
writeNativeHookJsonStdout(sanitizeCodexHookOutput(result.hookEventName, result.outputJson) ?? {});
|
|
5460
|
+
} else if (result.hookEventName !== "PreCompact" && result.hookEventName !== "PostCompact") {
|
|
4822
5461
|
writeNativeHookJsonStdout({});
|
|
4823
5462
|
}
|
|
4824
5463
|
} catch (error) {
|