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
|
@@ -214,6 +214,31 @@ if [[ "$cmd" == "capture-pane" ]]; then
|
|
|
214
214
|
printf '%s\\n' ${captureText}
|
|
215
215
|
exit 0
|
|
216
216
|
fi
|
|
217
|
+
if [[ "$cmd" == "set-buffer" ]]; then
|
|
218
|
+
printf '%s' "\${@: -1}" > "${tmuxLogPath}.buffer"
|
|
219
|
+
exit 0
|
|
220
|
+
fi
|
|
221
|
+
if [[ "$cmd" == "show-buffer" ]]; then
|
|
222
|
+
if [[ -f "${tmuxLogPath}.buffer" ]]; then cat "${tmuxLogPath}.buffer"; fi
|
|
223
|
+
exit 0
|
|
224
|
+
fi
|
|
225
|
+
if [[ "$cmd" == "paste-buffer" ]]; then
|
|
226
|
+
target=""
|
|
227
|
+
while [[ "$#" -gt 0 ]]; do
|
|
228
|
+
case "$1" in
|
|
229
|
+
-t) target="$2"; shift 2 ;;
|
|
230
|
+
*) shift ;;
|
|
231
|
+
esac
|
|
232
|
+
done
|
|
233
|
+
if [[ -f "${tmuxLogPath}.buffer" ]]; then
|
|
234
|
+
echo "send-keys -t \${target} -l $(cat "${tmuxLogPath}.buffer")" >> "${tmuxLogPath}"
|
|
235
|
+
fi
|
|
236
|
+
exit 0
|
|
237
|
+
fi
|
|
238
|
+
if [[ "$cmd" == "delete-buffer" ]]; then
|
|
239
|
+
rm -f "${tmuxLogPath}.buffer"
|
|
240
|
+
exit 0
|
|
241
|
+
fi
|
|
217
242
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
218
243
|
${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
|
|
219
244
|
${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
|
|
@@ -501,9 +526,9 @@ describe("codex native hook dispatch", () => {
|
|
|
501
526
|
hookSpecificOutput?: unknown;
|
|
502
527
|
};
|
|
503
528
|
|
|
504
|
-
assert.equal(output.continue,
|
|
529
|
+
assert.equal(output.continue, undefined);
|
|
505
530
|
assert.equal(output.decision, undefined);
|
|
506
|
-
assert.equal(output.stopReason,
|
|
531
|
+
assert.equal(output.stopReason, undefined);
|
|
507
532
|
assert.equal(output.hookSpecificOutput, undefined);
|
|
508
533
|
assert.match(
|
|
509
534
|
String(output.systemMessage ?? ""),
|
|
@@ -598,6 +623,159 @@ describe("codex native hook dispatch", () => {
|
|
|
598
623
|
);
|
|
599
624
|
});
|
|
600
625
|
|
|
626
|
+
it("emits no-op JSON stdout for PreToolUse non-Bash tools with null output", async () => {
|
|
627
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-nonbash-noop-"));
|
|
628
|
+
try {
|
|
629
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
630
|
+
cwd,
|
|
631
|
+
input: JSON.stringify({
|
|
632
|
+
hook_event_name: "PreToolUse",
|
|
633
|
+
cwd,
|
|
634
|
+
session_id: "sess-cli-pretool-nonbash-noop",
|
|
635
|
+
thread_id: "thread-cli-pretool-nonbash-noop",
|
|
636
|
+
turn_id: "turn-cli-pretool-nonbash-noop",
|
|
637
|
+
tool_name: "Read",
|
|
638
|
+
tool_input: { file_path: "package.json" },
|
|
639
|
+
}),
|
|
640
|
+
encoding: "utf-8",
|
|
641
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
645
|
+
assert.equal(result.stderr, "");
|
|
646
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
647
|
+
} finally {
|
|
648
|
+
await rm(cwd, { recursive: true, force: true });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("emits PreToolUse CLI advisory JSON with only systemMessage", async () => {
|
|
653
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-schema-safe-"));
|
|
654
|
+
try {
|
|
655
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
656
|
+
hook_event_name: "PreToolUse",
|
|
657
|
+
cwd,
|
|
658
|
+
session_id: "sess-cli-pretool-schema-safe",
|
|
659
|
+
thread_id: "thread-cli-pretool-schema-safe",
|
|
660
|
+
turn_id: "turn-cli-pretool-schema-safe",
|
|
661
|
+
tool_name: "Bash",
|
|
662
|
+
tool_input: { command: "rm -rf dist" },
|
|
663
|
+
}, { cwd }));
|
|
664
|
+
|
|
665
|
+
assert.deepEqual(output, {
|
|
666
|
+
systemMessage:
|
|
667
|
+
"Destructive Bash command detected (`rm -rf dist`). Confirm the target and expected side effects before running it.",
|
|
668
|
+
});
|
|
669
|
+
} finally {
|
|
670
|
+
await rm(cwd, { recursive: true, force: true });
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("emits PreToolUse CLI block JSON with only systemMessage", async () => {
|
|
675
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-block-schema-safe-"));
|
|
676
|
+
try {
|
|
677
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
678
|
+
hook_event_name: "PreToolUse",
|
|
679
|
+
cwd,
|
|
680
|
+
session_id: "sess-cli-pretool-block-schema-safe",
|
|
681
|
+
thread_id: "thread-cli-pretool-block-schema-safe",
|
|
682
|
+
turn_id: "turn-cli-pretool-block-schema-safe",
|
|
683
|
+
tool_name: "Bash",
|
|
684
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
|
|
685
|
+
}, { cwd }));
|
|
686
|
+
|
|
687
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
688
|
+
assert.match(String(output.systemMessage ?? ""), /Lore protocol/);
|
|
689
|
+
assert.equal(output.decision, undefined);
|
|
690
|
+
assert.equal(output.stopReason, undefined);
|
|
691
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
692
|
+
} finally {
|
|
693
|
+
await rm(cwd, { recursive: true, force: true });
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("preserves ralplan PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
|
|
698
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-pretool-boundary-"));
|
|
699
|
+
const sessionId = "sess-cli-ralplan-pretool-boundary";
|
|
700
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
701
|
+
try {
|
|
702
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
703
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
704
|
+
active: true,
|
|
705
|
+
skill: "ralplan",
|
|
706
|
+
phase: "planning",
|
|
707
|
+
session_id: sessionId,
|
|
708
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
709
|
+
});
|
|
710
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
711
|
+
active: true,
|
|
712
|
+
mode: "ralplan",
|
|
713
|
+
current_phase: "critic-review",
|
|
714
|
+
session_id: sessionId,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
718
|
+
hook_event_name: "PreToolUse",
|
|
719
|
+
cwd,
|
|
720
|
+
session_id: sessionId,
|
|
721
|
+
thread_id: "thread-cli-ralplan-pretool-boundary",
|
|
722
|
+
tool_name: "Edit",
|
|
723
|
+
tool_input: { file_path: "src/runtime.ts", old_string: "a", new_string: "b" },
|
|
724
|
+
}, { cwd }));
|
|
725
|
+
|
|
726
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
727
|
+
assert.match(String(output.systemMessage ?? ""), /Ralplan is active \(phase: critic-review\)/);
|
|
728
|
+
assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
|
|
729
|
+
assert.match(String(output.systemMessage ?? ""), /Write only planning artifacts/);
|
|
730
|
+
assert.equal(output.decision, undefined);
|
|
731
|
+
assert.equal(output.reason, undefined);
|
|
732
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
733
|
+
} finally {
|
|
734
|
+
await rm(cwd, { recursive: true, force: true });
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("preserves deep-interview PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
|
|
739
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-deep-interview-pretool-boundary-"));
|
|
740
|
+
const sessionId = "sess-cli-deep-interview-pretool-boundary";
|
|
741
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
742
|
+
try {
|
|
743
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
744
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
745
|
+
active: true,
|
|
746
|
+
skill: "deep-interview",
|
|
747
|
+
phase: "planning",
|
|
748
|
+
session_id: sessionId,
|
|
749
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId }],
|
|
750
|
+
});
|
|
751
|
+
await writeJson(join(stateDir, "sessions", sessionId, "deep-interview-state.json"), {
|
|
752
|
+
active: true,
|
|
753
|
+
mode: "deep-interview",
|
|
754
|
+
current_phase: "intent-first",
|
|
755
|
+
session_id: sessionId,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
759
|
+
hook_event_name: "PreToolUse",
|
|
760
|
+
cwd,
|
|
761
|
+
session_id: sessionId,
|
|
762
|
+
thread_id: "thread-cli-deep-interview-pretool-boundary",
|
|
763
|
+
tool_name: "Write",
|
|
764
|
+
tool_input: { file_path: "src/runtime.ts", content: "export const changed = true;\n" },
|
|
765
|
+
}, { cwd }));
|
|
766
|
+
|
|
767
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
768
|
+
assert.match(String(output.systemMessage ?? ""), /Deep-interview is active \(phase: intent-first\)/);
|
|
769
|
+
assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
|
|
770
|
+
assert.match(String(output.systemMessage ?? ""), /requirements\/spec mode/);
|
|
771
|
+
assert.equal(output.decision, undefined);
|
|
772
|
+
assert.equal(output.reason, undefined);
|
|
773
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
774
|
+
} finally {
|
|
775
|
+
await rm(cwd, { recursive: true, force: true });
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
601
779
|
it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
|
|
602
780
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
|
|
603
781
|
try {
|
|
@@ -617,6 +795,32 @@ describe("codex native hook dispatch", () => {
|
|
|
617
795
|
}
|
|
618
796
|
});
|
|
619
797
|
|
|
798
|
+
it("emits no-op JSON stdout for Stop payloads with no runtime output", async () => {
|
|
799
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-null-output-"));
|
|
800
|
+
try {
|
|
801
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
802
|
+
cwd,
|
|
803
|
+
input: JSON.stringify({
|
|
804
|
+
hook_event_name: "Stop",
|
|
805
|
+
cwd,
|
|
806
|
+
session_id: "sess-cli-stop-null-output",
|
|
807
|
+
thread_id: "thread-cli-stop-null-output",
|
|
808
|
+
turn_id: "turn-cli-stop-null-output",
|
|
809
|
+
stop_hook_active: true,
|
|
810
|
+
}),
|
|
811
|
+
encoding: "utf-8",
|
|
812
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
816
|
+
assert.equal(result.stderr, "");
|
|
817
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
818
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
819
|
+
} finally {
|
|
820
|
+
await rm(cwd, { recursive: true, force: true });
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
620
824
|
it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
|
|
621
825
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
|
|
622
826
|
try {
|
|
@@ -760,6 +964,83 @@ describe("codex native hook dispatch", () => {
|
|
|
760
964
|
}
|
|
761
965
|
});
|
|
762
966
|
|
|
967
|
+
it("does not block Stop on stale session autopilot mirror when canonical skill state is inactive", async () => {
|
|
968
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stale-autopilot-mirror-"));
|
|
969
|
+
try {
|
|
970
|
+
const sessionId = "sess-stale-autopilot-mirror";
|
|
971
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
972
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
973
|
+
version: 1,
|
|
974
|
+
active: false,
|
|
975
|
+
skill: "",
|
|
976
|
+
phase: "cancelled",
|
|
977
|
+
active_skills: [],
|
|
978
|
+
});
|
|
979
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
980
|
+
active: true,
|
|
981
|
+
mode: "autopilot",
|
|
982
|
+
current_phase: "ultragoal",
|
|
983
|
+
session_id: sessionId,
|
|
984
|
+
});
|
|
985
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "skill-active-state.json"), {
|
|
986
|
+
version: 1,
|
|
987
|
+
active: true,
|
|
988
|
+
skill: "autopilot",
|
|
989
|
+
phase: "ultragoal",
|
|
990
|
+
session_id: sessionId,
|
|
991
|
+
active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
const stdout = runNativeHookCli({
|
|
995
|
+
hook_event_name: "Stop",
|
|
996
|
+
cwd,
|
|
997
|
+
session_id: sessionId,
|
|
998
|
+
thread_id: "thread-stale-autopilot-mirror",
|
|
999
|
+
}, { cwd });
|
|
1000
|
+
|
|
1001
|
+
assert.deepEqual(parseSingleJsonStdout(stdout), {});
|
|
1002
|
+
} finally {
|
|
1003
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it("includes blocking state source and canonical agreement in Stop diagnostics", async () => {
|
|
1008
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-diagnostic-"));
|
|
1009
|
+
try {
|
|
1010
|
+
const sessionId = "sess-autopilot-diagnostic";
|
|
1011
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
1012
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
1013
|
+
version: 1,
|
|
1014
|
+
active: true,
|
|
1015
|
+
skill: "autopilot",
|
|
1016
|
+
phase: "ultragoal",
|
|
1017
|
+
session_id: sessionId,
|
|
1018
|
+
active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
|
|
1019
|
+
});
|
|
1020
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
1021
|
+
active: true,
|
|
1022
|
+
mode: "autopilot",
|
|
1023
|
+
current_phase: "ultragoal",
|
|
1024
|
+
session_id: sessionId,
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
1028
|
+
hook_event_name: "Stop",
|
|
1029
|
+
cwd,
|
|
1030
|
+
session_id: sessionId,
|
|
1031
|
+
thread_id: "thread-autopilot-diagnostic",
|
|
1032
|
+
}, { cwd })) as { decision?: string; reason?: string; statePath?: string; canonicalDisagreement?: string };
|
|
1033
|
+
|
|
1034
|
+
assert.equal(output.decision, "block");
|
|
1035
|
+
assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1036
|
+
assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
|
|
1037
|
+
assert.equal(output.statePath, ".omx/state/sessions/sess-autopilot-diagnostic/autopilot-state.json");
|
|
1038
|
+
assert.equal(output.canonicalDisagreement, "canonical_agrees");
|
|
1039
|
+
} finally {
|
|
1040
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
|
|
763
1044
|
it("emits exactly one parseable JSON object for active Stop CLI continuation", async () => {
|
|
764
1045
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-json-"));
|
|
765
1046
|
try {
|
|
@@ -2973,128 +3254,272 @@ standardMaxRounds = 15
|
|
|
2973
3254
|
}
|
|
2974
3255
|
});
|
|
2975
3256
|
|
|
2976
|
-
it("
|
|
2977
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3257
|
+
it("nudges next-goal prompts when a completed Codex goal cleanup step remains", async () => {
|
|
3258
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-prompt-"));
|
|
2978
3259
|
try {
|
|
2979
|
-
await writeJson(join(cwd, ".omx", "
|
|
3260
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
2980
3261
|
version: 1,
|
|
2981
|
-
|
|
2982
|
-
|
|
3262
|
+
workflow: "performance-goal",
|
|
3263
|
+
slug: "latency",
|
|
3264
|
+
objective: "Reduce latency",
|
|
3265
|
+
status: "complete",
|
|
3266
|
+
completedAt: "2026-05-20T00:00:00.000Z",
|
|
2983
3267
|
});
|
|
2984
3268
|
|
|
2985
3269
|
const result = await dispatchCodexNativeHook({
|
|
2986
|
-
hook_event_name: "
|
|
3270
|
+
hook_event_name: "UserPromptSubmit",
|
|
2987
3271
|
cwd,
|
|
2988
|
-
session_id: "sess-
|
|
2989
|
-
thread_id: "thread-
|
|
2990
|
-
|
|
3272
|
+
session_id: "sess-completed-goal-prompt",
|
|
3273
|
+
thread_id: "thread-completed-goal-prompt",
|
|
3274
|
+
prompt: "Start the next performance goal now",
|
|
2991
3275
|
}, { cwd });
|
|
2992
3276
|
|
|
2993
|
-
|
|
2994
|
-
assert.match(
|
|
3277
|
+
const output = JSON.stringify(result.outputJson);
|
|
3278
|
+
assert.match(output, /run \/goal clear/);
|
|
3279
|
+
assert.match(output, /before calling create_goal/);
|
|
3280
|
+
assert.match(output, /hooks only nudge and must not mutate Codex goal state/);
|
|
3281
|
+
assert.doesNotMatch(output, /cleared Codex goal state/i);
|
|
2995
3282
|
} finally {
|
|
2996
3283
|
await rm(cwd, { recursive: true, force: true });
|
|
2997
3284
|
}
|
|
2998
3285
|
});
|
|
2999
3286
|
|
|
3000
|
-
it("does not block
|
|
3001
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-ordinary-stop-"));
|
|
3287
|
+
it("does not block ordinary Stop after completed Ultragoal cleanup remains", async () => {
|
|
3288
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-ultragoal-ordinary-stop-"));
|
|
3002
3289
|
try {
|
|
3003
3290
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3004
3291
|
version: 1,
|
|
3005
|
-
|
|
3006
|
-
goals: [{ id: "G001-
|
|
3292
|
+
aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
|
|
3293
|
+
goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
|
|
3007
3294
|
});
|
|
3008
3295
|
|
|
3009
3296
|
const result = await dispatchCodexNativeHook({
|
|
3010
3297
|
hook_event_name: "Stop",
|
|
3011
3298
|
cwd,
|
|
3012
|
-
session_id: "sess-ultragoal-ordinary-stop",
|
|
3013
|
-
thread_id: "thread-ultragoal-ordinary-stop",
|
|
3014
|
-
last_assistant_message: "
|
|
3299
|
+
session_id: "sess-completed-ultragoal-ordinary-stop",
|
|
3300
|
+
thread_id: "thread-completed-ultragoal-ordinary-stop",
|
|
3301
|
+
last_assistant_message: "Implemented the requested fix and verified the focused tests.",
|
|
3015
3302
|
}, { cwd });
|
|
3016
3303
|
|
|
3017
|
-
|
|
3018
|
-
assert.
|
|
3304
|
+
const output = JSON.stringify(result.outputJson);
|
|
3305
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
3306
|
+
assert.notEqual(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
|
|
3307
|
+
assert.doesNotMatch(output, /run \/goal clear/);
|
|
3019
3308
|
} finally {
|
|
3020
3309
|
await rm(cwd, { recursive: true, force: true });
|
|
3021
3310
|
}
|
|
3022
3311
|
});
|
|
3023
3312
|
|
|
3024
|
-
it("
|
|
3025
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3313
|
+
it("does not treat explanatory create_goal warnings as completed-goal cleanup attempts", async () => {
|
|
3314
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-explainer-stop-"));
|
|
3026
3315
|
try {
|
|
3027
3316
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3028
3317
|
version: 1,
|
|
3029
|
-
|
|
3030
|
-
goals: [{ id: "G001-
|
|
3318
|
+
aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
|
|
3319
|
+
goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
|
|
3320
|
+
});
|
|
3321
|
+
|
|
3322
|
+
for (const [index, last_assistant_message] of [
|
|
3323
|
+
"Do not call create_goal until the user has explicitly cleared the old goal state.",
|
|
3324
|
+
"No create_goal attempt was made; this is only the final summary.",
|
|
3325
|
+
"I am not calling create_goal; the completed work is summarized above.",
|
|
3326
|
+
"I am not starting another goal; cleanup is already documented.",
|
|
3327
|
+
"Do not start another goal until cleanup is explicit.",
|
|
3328
|
+
"Do not create a new ultragoal; this is only a final summary.",
|
|
3329
|
+
].entries()) {
|
|
3330
|
+
const result = await dispatchCodexNativeHook({
|
|
3331
|
+
hook_event_name: "Stop",
|
|
3332
|
+
cwd,
|
|
3333
|
+
session_id: `sess-completed-goal-explainer-stop-${index}`,
|
|
3334
|
+
thread_id: `thread-completed-goal-explainer-stop-${index}`,
|
|
3335
|
+
last_assistant_message,
|
|
3336
|
+
}, { cwd });
|
|
3337
|
+
|
|
3338
|
+
const output = JSON.stringify(result.outputJson);
|
|
3339
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
3340
|
+
assert.notEqual(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
|
|
3341
|
+
assert.doesNotMatch(output, /run \/goal clear/);
|
|
3342
|
+
}
|
|
3343
|
+
} finally {
|
|
3344
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3345
|
+
}
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
it("blocks explicit create_goal attempts when fresh native-goal cleanup evidence remains", async () => {
|
|
3349
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-mixed-stop-"));
|
|
3350
|
+
try {
|
|
3351
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3352
|
+
version: 1,
|
|
3353
|
+
aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
|
|
3354
|
+
goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
|
|
3031
3355
|
});
|
|
3032
3356
|
|
|
3033
3357
|
const result = await dispatchCodexNativeHook({
|
|
3034
3358
|
hook_event_name: "Stop",
|
|
3035
3359
|
cwd,
|
|
3036
|
-
session_id: "sess-
|
|
3037
|
-
thread_id: "thread-
|
|
3038
|
-
|
|
3360
|
+
session_id: "sess-completed-goal-mixed-stop",
|
|
3361
|
+
thread_id: "thread-completed-goal-mixed-stop",
|
|
3362
|
+
last_user_message: "get_goal reports a completed Codex goal still attached to this thread; do not call create_goal until cleanup is explicit.",
|
|
3363
|
+
last_assistant_message: "I am starting another run now; create_goal payload follows.",
|
|
3039
3364
|
}, { cwd });
|
|
3040
3365
|
|
|
3041
3366
|
const output = JSON.stringify(result.outputJson);
|
|
3042
3367
|
assert.equal(result.outputJson?.decision, "block");
|
|
3043
|
-
assert.
|
|
3044
|
-
assert.match(output,
|
|
3045
|
-
assert.match(output, /Codex goal context/);
|
|
3046
|
-
assert.match(output, /no such table: thread_goals/);
|
|
3047
|
-
assert.match(output, /unavailable get_goal error JSON or path/);
|
|
3048
|
-
assert.match(output, /safe-recovery blocker/);
|
|
3049
|
-
assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
|
|
3050
|
-
assert.match(output, /Hooks must not mutate Codex goal state/);
|
|
3368
|
+
assert.equal(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
|
|
3369
|
+
assert.match(output, /run \/goal clear/);
|
|
3051
3370
|
} finally {
|
|
3052
3371
|
await rm(cwd, { recursive: true, force: true });
|
|
3053
3372
|
}
|
|
3054
3373
|
});
|
|
3055
3374
|
|
|
3056
|
-
it("does not
|
|
3057
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3375
|
+
it("does not block Stop when completed durable history alone precedes another goal", async () => {
|
|
3376
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-stop-"));
|
|
3058
3377
|
try {
|
|
3059
3378
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3060
3379
|
version: 1,
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
goals: [{
|
|
3064
|
-
id: "G001-demo",
|
|
3065
|
-
status: "in_progress",
|
|
3066
|
-
objective: "Demo goal",
|
|
3067
|
-
failureReason: "aggregate Codex goal already complete and unreconcilable while repo-native .omx/ultragoal/goals.json still has an in-progress microgoal; stop the recovery loop",
|
|
3068
|
-
}],
|
|
3380
|
+
aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
|
|
3381
|
+
goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
|
|
3069
3382
|
});
|
|
3070
3383
|
|
|
3071
3384
|
const result = await dispatchCodexNativeHook({
|
|
3072
3385
|
hook_event_name: "Stop",
|
|
3073
3386
|
cwd,
|
|
3074
|
-
session_id: "sess-
|
|
3075
|
-
thread_id: "thread-
|
|
3076
|
-
|
|
3077
|
-
last_assistant_message: "Goal complete.",
|
|
3387
|
+
session_id: "sess-completed-goal-stop",
|
|
3388
|
+
thread_id: "thread-completed-goal-stop",
|
|
3389
|
+
last_assistant_message: "Starting another ultragoal now; create_goal payload follows after /goal clear already cleared native state.",
|
|
3078
3390
|
}, { cwd });
|
|
3079
3391
|
|
|
3392
|
+
const output = JSON.stringify(result.outputJson);
|
|
3080
3393
|
assert.notEqual(result.outputJson?.decision, "block");
|
|
3081
|
-
assert.notEqual(result.outputJson?.stopReason, "
|
|
3082
|
-
assert.doesNotMatch(
|
|
3394
|
+
assert.notEqual(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
|
|
3395
|
+
assert.doesNotMatch(output, /run \/goal clear/);
|
|
3083
3396
|
} finally {
|
|
3084
3397
|
await rm(cwd, { recursive: true, force: true });
|
|
3085
3398
|
}
|
|
3086
3399
|
});
|
|
3087
3400
|
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
|
|
3401
|
+
it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
|
|
3402
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
|
|
3091
3403
|
try {
|
|
3092
3404
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3093
3405
|
version: 1,
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3406
|
+
activeGoalId: "G001-demo",
|
|
3407
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
const result = await dispatchCodexNativeHook({
|
|
3411
|
+
hook_event_name: "Stop",
|
|
3412
|
+
cwd,
|
|
3413
|
+
session_id: "sess-ultragoal-generic-complete-stop",
|
|
3414
|
+
thread_id: "thread-ultragoal-generic-complete-stop",
|
|
3415
|
+
last_assistant_message: "Goal complete.",
|
|
3416
|
+
}, { cwd });
|
|
3417
|
+
|
|
3418
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
3419
|
+
assert.match(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
3420
|
+
} finally {
|
|
3421
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3424
|
+
|
|
3425
|
+
it("does not block ultragoal Stop for ordinary prose about a goal to complete work", async () => {
|
|
3426
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-ordinary-stop-"));
|
|
3427
|
+
try {
|
|
3428
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3429
|
+
version: 1,
|
|
3430
|
+
activeGoalId: "G001-demo",
|
|
3431
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
3432
|
+
});
|
|
3433
|
+
|
|
3434
|
+
const result = await dispatchCodexNativeHook({
|
|
3435
|
+
hook_event_name: "Stop",
|
|
3436
|
+
cwd,
|
|
3437
|
+
session_id: "sess-ultragoal-ordinary-stop",
|
|
3438
|
+
thread_id: "thread-ultragoal-ordinary-stop",
|
|
3439
|
+
last_assistant_message: "My goal is to complete the migration without regressions, so I will keep testing.",
|
|
3440
|
+
}, { cwd });
|
|
3441
|
+
|
|
3442
|
+
assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
|
|
3443
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
3444
|
+
} finally {
|
|
3445
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
it("blocks ultragoal Stop with blocked checkpoint and available-goal-context remediation for completed legacy snapshots", async () => {
|
|
3450
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
|
|
3451
|
+
try {
|
|
3452
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3453
|
+
version: 1,
|
|
3454
|
+
activeGoalId: "G001-demo",
|
|
3455
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
3456
|
+
});
|
|
3457
|
+
|
|
3458
|
+
const result = await dispatchCodexNativeHook({
|
|
3459
|
+
hook_event_name: "Stop",
|
|
3460
|
+
cwd,
|
|
3461
|
+
session_id: "sess-ultragoal-legacy-stop",
|
|
3462
|
+
thread_id: "thread-ultragoal-legacy-stop",
|
|
3463
|
+
last_assistant_message: "get_goal returned a completed legacy goal, so ultragoal complete failed; marking complete now.",
|
|
3464
|
+
}, { cwd });
|
|
3465
|
+
|
|
3466
|
+
const output = JSON.stringify(result.outputJson);
|
|
3467
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
3468
|
+
assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
3469
|
+
assert.match(output, /--status blocked/);
|
|
3470
|
+
assert.match(output, /Codex goal context/);
|
|
3471
|
+
assert.match(output, /no such table: thread_goals/);
|
|
3472
|
+
assert.match(output, /unavailable get_goal error JSON or path/);
|
|
3473
|
+
assert.match(output, /safe-recovery blocker/);
|
|
3474
|
+
assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
|
|
3475
|
+
assert.match(output, /Hooks must not mutate Codex goal state/);
|
|
3476
|
+
} finally {
|
|
3477
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3478
|
+
}
|
|
3479
|
+
});
|
|
3480
|
+
|
|
3481
|
+
it("does not repeat ultragoal Stop recovery after a safe completed-aggregate microgoal blocker is recorded", async () => {
|
|
3482
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-aggregate-blocked-stop-"));
|
|
3483
|
+
try {
|
|
3484
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3485
|
+
version: 1,
|
|
3486
|
+
codexGoalMode: "aggregate",
|
|
3487
|
+
activeGoalId: "G001-demo",
|
|
3488
|
+
goals: [{
|
|
3489
|
+
id: "G001-demo",
|
|
3490
|
+
status: "in_progress",
|
|
3491
|
+
objective: "Demo goal",
|
|
3492
|
+
failureReason: "aggregate Codex goal already complete and unreconcilable while repo-native .omx/ultragoal/goals.json still has an in-progress microgoal; stop the recovery loop",
|
|
3493
|
+
}],
|
|
3494
|
+
});
|
|
3495
|
+
|
|
3496
|
+
const result = await dispatchCodexNativeHook({
|
|
3497
|
+
hook_event_name: "Stop",
|
|
3498
|
+
cwd,
|
|
3499
|
+
session_id: "sess-ultragoal-aggregate-blocked-stop",
|
|
3500
|
+
thread_id: "thread-ultragoal-aggregate-blocked-stop",
|
|
3501
|
+
stop_hook_active: true,
|
|
3502
|
+
last_assistant_message: "Goal complete.",
|
|
3503
|
+
}, { cwd });
|
|
3504
|
+
|
|
3505
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
3506
|
+
assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
|
|
3507
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
3508
|
+
} finally {
|
|
3509
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
3512
|
+
|
|
3513
|
+
|
|
3514
|
+
it("does not block ultragoal Stop after task-scoped reconciliation finishes exploded bookkeeping", async () => {
|
|
3515
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
|
|
3516
|
+
try {
|
|
3517
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3518
|
+
version: 1,
|
|
3519
|
+
codexGoalMode: "aggregate",
|
|
3520
|
+
codexObjective: "Complete the durable ultragoal plan in .omx/ultragoal/goals.json, including later accepted/appended stories, under the original brief constraints; use .omx/ultragoal/ledger.jsonl as the audit trail.",
|
|
3521
|
+
activeGoalId: "G001-micro",
|
|
3522
|
+
aggregateCompletion: {
|
|
3098
3523
|
status: "complete",
|
|
3099
3524
|
completedAt: "2026-05-04T10:04:00.000Z",
|
|
3100
3525
|
evidence: "planned work done; validation complete; reviews clean",
|
|
@@ -6381,6 +6806,144 @@ exit 0
|
|
|
6381
6806
|
}
|
|
6382
6807
|
});
|
|
6383
6808
|
|
|
6809
|
+
it("allows Ralplan read-only inspection and planning artifact writes while blocking implementation targets", async () => {
|
|
6810
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-guard-"));
|
|
6811
|
+
try {
|
|
6812
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6813
|
+
const sessionId = "sess-ralplan-guard";
|
|
6814
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
6815
|
+
await mkdir(sessionDir, { recursive: true });
|
|
6816
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
6817
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6818
|
+
version: 1,
|
|
6819
|
+
active: true,
|
|
6820
|
+
skill: "ralplan",
|
|
6821
|
+
phase: "planning",
|
|
6822
|
+
session_id: sessionId,
|
|
6823
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
6824
|
+
});
|
|
6825
|
+
await writeJson(join(sessionDir, "ralplan-state.json"), {
|
|
6826
|
+
active: true,
|
|
6827
|
+
mode: "ralplan",
|
|
6828
|
+
current_phase: "planning",
|
|
6829
|
+
session_id: sessionId,
|
|
6830
|
+
});
|
|
6831
|
+
|
|
6832
|
+
const preToolUse = async (tool_name: string, tool_use_id: string, tool_input: Record<string, unknown>) => dispatchCodexNativeHook(
|
|
6833
|
+
{
|
|
6834
|
+
hook_event_name: "PreToolUse",
|
|
6835
|
+
cwd,
|
|
6836
|
+
session_id: sessionId,
|
|
6837
|
+
tool_name,
|
|
6838
|
+
tool_use_id,
|
|
6839
|
+
tool_input,
|
|
6840
|
+
},
|
|
6841
|
+
{ cwd },
|
|
6842
|
+
);
|
|
6843
|
+
|
|
6844
|
+
const readOnlyCommands = [
|
|
6845
|
+
"git status --short",
|
|
6846
|
+
"ls -1 .omx/plans",
|
|
6847
|
+
"find .omx/plans -type f -name '*.md'",
|
|
6848
|
+
"grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
6849
|
+
"rg \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
6850
|
+
"sed -n '1,80p' .omx/plans/demo.md",
|
|
6851
|
+
"cat .omx/plans/demo.md",
|
|
6852
|
+
"git status --short; ls -1 .omx/plans; grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
6853
|
+
];
|
|
6854
|
+
for (const [index, command] of readOnlyCommands.entries()) {
|
|
6855
|
+
const result = await preToolUse("Bash", `tool-ralplan-read-${index}`, { command });
|
|
6856
|
+
assert.equal(result.outputJson, null, `read-only command should be allowed: ${command}`);
|
|
6857
|
+
}
|
|
6858
|
+
|
|
6859
|
+
for (const path of [
|
|
6860
|
+
".omx/context/findings.md",
|
|
6861
|
+
".omx/plans/issue-2863.md",
|
|
6862
|
+
".omx/specs/issue-2863.md",
|
|
6863
|
+
".omx/state/required-planning-state.json",
|
|
6864
|
+
]) {
|
|
6865
|
+
const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
|
|
6866
|
+
assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
|
|
6867
|
+
}
|
|
6868
|
+
|
|
6869
|
+
const allowedPatchAdd = await preToolUse("apply_patch", "tool-ralplan-patch-add", {
|
|
6870
|
+
input: "*** Begin Patch\n*** Add File: .omx/plans/issue-2863.md\n+# Plan\n*** End Patch\n",
|
|
6871
|
+
});
|
|
6872
|
+
assert.equal(allowedPatchAdd.outputJson, null);
|
|
6873
|
+
|
|
6874
|
+
const allowedPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-patch-update", {
|
|
6875
|
+
input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
6876
|
+
});
|
|
6877
|
+
assert.equal(allowedPatchUpdate.outputJson, null);
|
|
6878
|
+
|
|
6879
|
+
const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
|
|
6880
|
+
command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
|
|
6881
|
+
});
|
|
6882
|
+
assert.equal(allowedRedirect.outputJson, null);
|
|
6883
|
+
|
|
6884
|
+
const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
|
|
6885
|
+
command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
|
|
6886
|
+
});
|
|
6887
|
+
assert.equal(allowedTee.outputJson, null);
|
|
6888
|
+
|
|
6889
|
+
const blockedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-block", {
|
|
6890
|
+
command: "printf 'bad' > src/implementation.ts",
|
|
6891
|
+
});
|
|
6892
|
+
assert.equal((blockedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6893
|
+
const redirectReason = String((blockedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6894
|
+
assert.match(redirectReason, /Bash redirect write/);
|
|
6895
|
+
assert.match(redirectReason, /src\/implementation\.ts/);
|
|
6896
|
+
|
|
6897
|
+
const blockedEdit = await preToolUse("Edit", "tool-ralplan-edit-block", {
|
|
6898
|
+
file_path: "src/implementation.ts",
|
|
6899
|
+
old_string: "a",
|
|
6900
|
+
new_string: "b",
|
|
6901
|
+
});
|
|
6902
|
+
assert.equal((blockedEdit.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6903
|
+
const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6904
|
+
assert.match(editReason, /Edit path/);
|
|
6905
|
+
assert.match(editReason, /src\/implementation\.ts/);
|
|
6906
|
+
|
|
6907
|
+
const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
|
|
6908
|
+
input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
6909
|
+
});
|
|
6910
|
+
assert.equal((blockedMixedPatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6911
|
+
const mixedReason = String((blockedMixedPatch.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6912
|
+
assert.match(mixedReason, /apply_patch target/);
|
|
6913
|
+
assert.match(mixedReason, /src\/leak\.ts/);
|
|
6914
|
+
|
|
6915
|
+
const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
|
|
6916
|
+
input: "not a recognizable patch",
|
|
6917
|
+
});
|
|
6918
|
+
assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6919
|
+
assert.match(String((blockedUnparseablePatch.outputJson as { reason?: string } | null)?.reason ?? ""), /apply_patch target extraction failed/);
|
|
6920
|
+
|
|
6921
|
+
const blockedUnresolvedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-unresolved", {
|
|
6922
|
+
command: "cat > \"$PLAN_PATH\" <<'EOF'\ncontent\nEOF",
|
|
6923
|
+
});
|
|
6924
|
+
assert.equal((blockedUnresolvedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6925
|
+
const unresolvedReason = String((blockedUnresolvedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6926
|
+
assert.match(unresolvedReason, /unresolved Bash write target/);
|
|
6927
|
+
assert.match(unresolvedReason, /\$PLAN_PATH/);
|
|
6928
|
+
|
|
6929
|
+
const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
|
|
6930
|
+
file_path: ".omx/plans/../../src/leak.ts",
|
|
6931
|
+
content: "bad",
|
|
6932
|
+
});
|
|
6933
|
+
assert.equal((blockedTraversal.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6934
|
+
assert.match(String((blockedTraversal.outputJson as { reason?: string } | null)?.reason ?? ""), /\.omx\/plans\/\.\.\/\.\.\/src\/leak\.ts/);
|
|
6935
|
+
|
|
6936
|
+
const blockedAbsolute = await preToolUse("Write", "tool-ralplan-absolute-block", {
|
|
6937
|
+
file_path: "/tmp/leak.ts",
|
|
6938
|
+
content: "bad",
|
|
6939
|
+
});
|
|
6940
|
+
assert.equal((blockedAbsolute.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6941
|
+
assert.match(String((blockedAbsolute.outputJson as { reason?: string } | null)?.reason ?? ""), /\/tmp\/leak\.ts/);
|
|
6942
|
+
} finally {
|
|
6943
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6944
|
+
}
|
|
6945
|
+
});
|
|
6946
|
+
|
|
6384
6947
|
it("allows Autopilot ralplan planning artifacts while blocking implementation writes", async () => {
|
|
6385
6948
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-ralplan-artifact-"));
|
|
6386
6949
|
try {
|
|
@@ -6461,87 +7024,344 @@ exit 0
|
|
|
6461
7024
|
}
|
|
6462
7025
|
});
|
|
6463
7026
|
|
|
6464
|
-
it("allows
|
|
6465
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
7027
|
+
it("allows Ralplan Beads tracker metadata writes during planning", async () => {
|
|
7028
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-beads-"));
|
|
6466
7029
|
try {
|
|
6467
7030
|
const stateDir = join(cwd, ".omx", "state");
|
|
6468
|
-
const sessionDir = join(stateDir, "sessions", "sess-
|
|
7031
|
+
const sessionDir = join(stateDir, "sessions", "sess-ralplan-beads");
|
|
6469
7032
|
await mkdir(sessionDir, { recursive: true });
|
|
6470
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-
|
|
7033
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-ralplan-beads", cwd });
|
|
6471
7034
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6472
7035
|
version: 1,
|
|
6473
7036
|
active: true,
|
|
6474
|
-
skill: "
|
|
7037
|
+
skill: "ralplan",
|
|
6475
7038
|
phase: "planning",
|
|
6476
|
-
session_id: "sess-
|
|
6477
|
-
|
|
7039
|
+
session_id: "sess-ralplan-beads",
|
|
7040
|
+
thread_id: "thread-ralplan-beads",
|
|
7041
|
+
active_skills: [
|
|
7042
|
+
{
|
|
7043
|
+
skill: "ralplan",
|
|
7044
|
+
phase: "planning",
|
|
7045
|
+
active: true,
|
|
7046
|
+
session_id: "sess-ralplan-beads",
|
|
7047
|
+
thread_id: "thread-ralplan-beads",
|
|
7048
|
+
},
|
|
7049
|
+
],
|
|
6478
7050
|
});
|
|
6479
|
-
await writeJson(join(sessionDir, "
|
|
7051
|
+
await writeJson(join(sessionDir, "ralplan-state.json"), {
|
|
6480
7052
|
active: true,
|
|
6481
|
-
mode: "
|
|
6482
|
-
current_phase: "
|
|
6483
|
-
session_id: "sess-
|
|
7053
|
+
mode: "ralplan",
|
|
7054
|
+
current_phase: "planning",
|
|
7055
|
+
session_id: "sess-ralplan-beads",
|
|
7056
|
+
thread_id: "thread-ralplan-beads",
|
|
6484
7057
|
});
|
|
6485
7058
|
|
|
6486
|
-
const
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
7059
|
+
const allowedWrite = await dispatchCodexNativeHook(
|
|
7060
|
+
{
|
|
7061
|
+
hook_event_name: "PreToolUse",
|
|
7062
|
+
cwd,
|
|
7063
|
+
session_id: "sess-ralplan-beads",
|
|
7064
|
+
thread_id: "thread-ralplan-beads",
|
|
7065
|
+
tool_name: "Write",
|
|
7066
|
+
tool_use_id: "tool-ralplan-beads-write",
|
|
7067
|
+
tool_input: { file_path: ".beads/issues.db", content: "metadata" },
|
|
7068
|
+
},
|
|
7069
|
+
{ cwd },
|
|
7070
|
+
);
|
|
7071
|
+
assert.equal(allowedWrite.outputJson, null);
|
|
6493
7072
|
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
7073
|
+
const allowedEdit = await dispatchCodexNativeHook(
|
|
7074
|
+
{
|
|
7075
|
+
hook_event_name: "PreToolUse",
|
|
7076
|
+
cwd,
|
|
7077
|
+
session_id: "sess-ralplan-beads",
|
|
7078
|
+
thread_id: "thread-ralplan-beads",
|
|
7079
|
+
tool_name: "Edit",
|
|
7080
|
+
tool_use_id: "tool-ralplan-beads-edit",
|
|
7081
|
+
tool_input: { file_path: ".beads/tasks/issue.json", old_string: "old", new_string: "new" },
|
|
7082
|
+
},
|
|
7083
|
+
{ cwd },
|
|
7084
|
+
);
|
|
7085
|
+
assert.equal(allowedEdit.outputJson, null);
|
|
7086
|
+
|
|
7087
|
+
const allowedPatch = await dispatchCodexNativeHook(
|
|
7088
|
+
{
|
|
7089
|
+
hook_event_name: "PreToolUse",
|
|
7090
|
+
cwd,
|
|
7091
|
+
session_id: "sess-ralplan-beads",
|
|
7092
|
+
thread_id: "thread-ralplan-beads",
|
|
7093
|
+
tool_name: "apply_patch",
|
|
7094
|
+
tool_use_id: "tool-ralplan-beads-apply-patch",
|
|
7095
|
+
tool_input: {
|
|
7096
|
+
input: "*** Begin Patch\n*** Add File: .beads/tasks/issue.json\n+{}\n*** End Patch\n",
|
|
6503
7097
|
},
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
7098
|
+
},
|
|
7099
|
+
{ cwd },
|
|
7100
|
+
);
|
|
7101
|
+
assert.equal(allowedPatch.outputJson, null);
|
|
6508
7102
|
|
|
6509
|
-
const
|
|
6510
|
-
"
|
|
6511
|
-
"
|
|
6512
|
-
"
|
|
7103
|
+
const allowedCommands = [
|
|
7104
|
+
"bd --db .beads/issues.db create 'Issue title'",
|
|
7105
|
+
"bd --db .beads/issues.db update OMX-1 --title 'Issue title'",
|
|
7106
|
+
"bd --db .beads/issues.db edit OMX-1 --title 'Issue title'",
|
|
7107
|
+
"bd --db .beads/issues.db comments add OMX-1 'evidence'",
|
|
7108
|
+
"bd --db .beads/issues.db close OMX-1",
|
|
7109
|
+
"bd --db .beads/issues.db reopen OMX-1",
|
|
7110
|
+
"bd --db .beads/issues.db status OMX-1",
|
|
7111
|
+
"bd --db .beads/issues.db dep add OMX-1 OMX-2",
|
|
6513
7112
|
];
|
|
6514
7113
|
|
|
6515
|
-
for (const [index, command] of
|
|
7114
|
+
for (const [index, command] of allowedCommands.entries()) {
|
|
6516
7115
|
const result = await dispatchCodexNativeHook(
|
|
6517
7116
|
{
|
|
6518
7117
|
hook_event_name: "PreToolUse",
|
|
6519
7118
|
cwd,
|
|
6520
|
-
session_id: "sess-
|
|
7119
|
+
session_id: "sess-ralplan-beads",
|
|
7120
|
+
thread_id: "thread-ralplan-beads",
|
|
6521
7121
|
tool_name: "Bash",
|
|
6522
|
-
tool_use_id: `tool-
|
|
7122
|
+
tool_use_id: `tool-ralplan-beads-bd-${index}`,
|
|
6523
7123
|
tool_input: { command },
|
|
6524
7124
|
},
|
|
6525
7125
|
{ cwd },
|
|
6526
7126
|
);
|
|
6527
|
-
assert.equal(
|
|
7127
|
+
assert.equal(result.outputJson, null, `expected Beads metadata command to be allowed: ${command}`);
|
|
6528
7128
|
}
|
|
6529
7129
|
} finally {
|
|
6530
7130
|
await rm(cwd, { recursive: true, force: true });
|
|
6531
7131
|
}
|
|
6532
7132
|
});
|
|
6533
7133
|
|
|
6534
|
-
it("
|
|
6535
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
7134
|
+
it("blocks unsafe Beads tracker targets and mixed implementation writes during Autopilot ralplan planning", async () => {
|
|
7135
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-beads-block-"));
|
|
6536
7136
|
try {
|
|
6537
7137
|
const stateDir = join(cwd, ".omx", "state");
|
|
6538
|
-
const sessionDir = join(stateDir, "sessions", "sess-
|
|
7138
|
+
const sessionDir = join(stateDir, "sessions", "sess-autopilot-beads-block");
|
|
6539
7139
|
await mkdir(sessionDir, { recursive: true });
|
|
7140
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-beads-block", cwd });
|
|
6540
7141
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6541
7142
|
version: 1,
|
|
6542
7143
|
active: true,
|
|
6543
|
-
skill: "
|
|
6544
|
-
phase: "
|
|
7144
|
+
skill: "autopilot",
|
|
7145
|
+
phase: "ralplan",
|
|
7146
|
+
session_id: "sess-autopilot-beads-block",
|
|
7147
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7148
|
+
active_skills: [
|
|
7149
|
+
{
|
|
7150
|
+
skill: "autopilot",
|
|
7151
|
+
phase: "ralplan",
|
|
7152
|
+
active: true,
|
|
7153
|
+
session_id: "sess-autopilot-beads-block",
|
|
7154
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7155
|
+
},
|
|
7156
|
+
],
|
|
7157
|
+
});
|
|
7158
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
7159
|
+
active: true,
|
|
7160
|
+
mode: "autopilot",
|
|
7161
|
+
current_phase: "ralplan",
|
|
7162
|
+
session_id: "sess-autopilot-beads-block",
|
|
7163
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7164
|
+
});
|
|
7165
|
+
|
|
7166
|
+
const blockedCommands = [
|
|
7167
|
+
"bd --db /tmp/outside.db create 'Issue title'",
|
|
7168
|
+
"bd --db ../outside.db create 'Issue title'",
|
|
7169
|
+
"bd --db .beads create 'Issue title'",
|
|
7170
|
+
"bd --db $DB create 'Issue title'",
|
|
7171
|
+
"DB=.beads/issues.db bd --db $DB create 'Issue title'",
|
|
7172
|
+
"env bd --db /tmp/outside.db create 'Issue title'",
|
|
7173
|
+
"command bd --db /tmp/outside.db create 'Issue title'",
|
|
7174
|
+
"bd --db .beads/issues.db export",
|
|
7175
|
+
"bd --db .beads/issues.db create 'Issue title' > src/leak.ts",
|
|
7176
|
+
"bd --db .beads/issues.db create 'Issue title'; cat > src/leak.ts",
|
|
7177
|
+
];
|
|
7178
|
+
|
|
7179
|
+
for (const [index, command] of blockedCommands.entries()) {
|
|
7180
|
+
const result = await dispatchCodexNativeHook(
|
|
7181
|
+
{
|
|
7182
|
+
hook_event_name: "PreToolUse",
|
|
7183
|
+
cwd,
|
|
7184
|
+
session_id: "sess-autopilot-beads-block",
|
|
7185
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7186
|
+
tool_name: "Bash",
|
|
7187
|
+
tool_use_id: `tool-autopilot-beads-block-${index}`,
|
|
7188
|
+
tool_input: { command },
|
|
7189
|
+
},
|
|
7190
|
+
{ cwd },
|
|
7191
|
+
);
|
|
7192
|
+
assert.equal(
|
|
7193
|
+
(result.outputJson as { decision?: string } | null)?.decision,
|
|
7194
|
+
"block",
|
|
7195
|
+
`expected unsafe Beads command to be blocked: ${command}`,
|
|
7196
|
+
);
|
|
7197
|
+
}
|
|
7198
|
+
|
|
7199
|
+
const mentionWithImplementationWrite = await dispatchCodexNativeHook(
|
|
7200
|
+
{
|
|
7201
|
+
hook_event_name: "PreToolUse",
|
|
7202
|
+
cwd,
|
|
7203
|
+
session_id: "sess-autopilot-beads-block",
|
|
7204
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7205
|
+
tool_name: "Bash",
|
|
7206
|
+
tool_use_id: "tool-autopilot-beads-mention-write",
|
|
7207
|
+
tool_input: { command: "echo bd --db .beads/issues.db create > src/leak.ts" },
|
|
7208
|
+
},
|
|
7209
|
+
{ cwd },
|
|
7210
|
+
);
|
|
7211
|
+
assert.equal((mentionWithImplementationWrite.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7212
|
+
assert.match(String((mentionWithImplementationWrite.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
|
|
7213
|
+
|
|
7214
|
+
const blockedImplementationEdit = await dispatchCodexNativeHook(
|
|
7215
|
+
{
|
|
7216
|
+
hook_event_name: "PreToolUse",
|
|
7217
|
+
cwd,
|
|
7218
|
+
session_id: "sess-autopilot-beads-block",
|
|
7219
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7220
|
+
tool_name: "Edit",
|
|
7221
|
+
tool_use_id: "tool-autopilot-beads-src-edit",
|
|
7222
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
7223
|
+
},
|
|
7224
|
+
{ cwd },
|
|
7225
|
+
);
|
|
7226
|
+
assert.equal((blockedImplementationEdit.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7227
|
+
} finally {
|
|
7228
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7229
|
+
}
|
|
7230
|
+
});
|
|
7231
|
+
|
|
7232
|
+
it("allows Autopilot rework implementation writes while ralplan remains guarded", async () => {
|
|
7233
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-rework-"));
|
|
7234
|
+
try {
|
|
7235
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7236
|
+
const sessionDir = join(stateDir, "sessions", "sess-autopilot-rework");
|
|
7237
|
+
await mkdir(sessionDir, { recursive: true });
|
|
7238
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-rework", cwd });
|
|
7239
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
7240
|
+
version: 1,
|
|
7241
|
+
active: true,
|
|
7242
|
+
skill: "autopilot",
|
|
7243
|
+
phase: "rework",
|
|
7244
|
+
session_id: "sess-autopilot-rework",
|
|
7245
|
+
thread_id: "thread-autopilot-rework",
|
|
7246
|
+
active_skills: [
|
|
7247
|
+
{
|
|
7248
|
+
skill: "autopilot",
|
|
7249
|
+
phase: "rework",
|
|
7250
|
+
active: true,
|
|
7251
|
+
session_id: "sess-autopilot-rework",
|
|
7252
|
+
thread_id: "thread-autopilot-rework",
|
|
7253
|
+
},
|
|
7254
|
+
],
|
|
7255
|
+
});
|
|
7256
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
7257
|
+
active: true,
|
|
7258
|
+
mode: "autopilot",
|
|
7259
|
+
current_phase: "rework",
|
|
7260
|
+
review_cycle: 2,
|
|
7261
|
+
review_verdict: { clean: false, recommendation: "REQUEST_CHANGES", findings: ["src/implementation.ts"] },
|
|
7262
|
+
session_id: "sess-autopilot-rework",
|
|
7263
|
+
thread_id: "thread-autopilot-rework",
|
|
7264
|
+
});
|
|
7265
|
+
|
|
7266
|
+
const allowedImplementationEdit = await dispatchCodexNativeHook(
|
|
7267
|
+
{
|
|
7268
|
+
hook_event_name: "PreToolUse",
|
|
7269
|
+
cwd,
|
|
7270
|
+
session_id: "sess-autopilot-rework",
|
|
7271
|
+
thread_id: "thread-autopilot-rework",
|
|
7272
|
+
tool_name: "Edit",
|
|
7273
|
+
tool_use_id: "tool-autopilot-rework-src-edit",
|
|
7274
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
7275
|
+
},
|
|
7276
|
+
{ cwd },
|
|
7277
|
+
);
|
|
7278
|
+
assert.equal(allowedImplementationEdit.outputJson, null);
|
|
7279
|
+
} finally {
|
|
7280
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7281
|
+
}
|
|
7282
|
+
});
|
|
7283
|
+
|
|
7284
|
+
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
7285
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
7286
|
+
try {
|
|
7287
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7288
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-null-redirect");
|
|
7289
|
+
await mkdir(sessionDir, { recursive: true });
|
|
7290
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-null-redirect", cwd });
|
|
7291
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
7292
|
+
version: 1,
|
|
7293
|
+
active: true,
|
|
7294
|
+
skill: "deep-interview",
|
|
7295
|
+
phase: "planning",
|
|
7296
|
+
session_id: "sess-di-null-redirect",
|
|
7297
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-null-redirect" }],
|
|
7298
|
+
});
|
|
7299
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
7300
|
+
active: true,
|
|
7301
|
+
mode: "deep-interview",
|
|
7302
|
+
current_phase: "intent-first",
|
|
7303
|
+
session_id: "sess-di-null-redirect",
|
|
7304
|
+
});
|
|
7305
|
+
|
|
7306
|
+
const allowedCommands = [
|
|
7307
|
+
"find application -type d -name 'bug-tracking*' 2>/dev/null | head -20",
|
|
7308
|
+
"find application -type d -name 'bug-tracking*' 2> /dev/null | head -20",
|
|
7309
|
+
"find application -type d -name 'bug-tracking*' 2>NUL | head -20",
|
|
7310
|
+
"find application -type d -name 'bug-tracking*' 1>/dev/null",
|
|
7311
|
+
"find application -type d -name 'bug-tracking*' &>/dev/null",
|
|
7312
|
+
];
|
|
7313
|
+
|
|
7314
|
+
for (const [index, command] of allowedCommands.entries()) {
|
|
7315
|
+
const result = await dispatchCodexNativeHook(
|
|
7316
|
+
{
|
|
7317
|
+
hook_event_name: "PreToolUse",
|
|
7318
|
+
cwd,
|
|
7319
|
+
session_id: "sess-di-null-redirect",
|
|
7320
|
+
tool_name: "Bash",
|
|
7321
|
+
tool_use_id: `tool-di-null-redirect-${index}`,
|
|
7322
|
+
tool_input: { command },
|
|
7323
|
+
},
|
|
7324
|
+
{ cwd },
|
|
7325
|
+
);
|
|
7326
|
+
assert.equal(result.outputJson, null, command);
|
|
7327
|
+
}
|
|
7328
|
+
|
|
7329
|
+
const blockedCommands = [
|
|
7330
|
+
"find application -type d -name 'bug-tracking*' 2>errors.log | head -20",
|
|
7331
|
+
"find application -type d -name 'bug-tracking*' > /tmp/bug-tracking.txt",
|
|
7332
|
+
"find application -type d -name 'bug-tracking*' | tee /dev/null",
|
|
7333
|
+
];
|
|
7334
|
+
|
|
7335
|
+
for (const [index, command] of blockedCommands.entries()) {
|
|
7336
|
+
const result = await dispatchCodexNativeHook(
|
|
7337
|
+
{
|
|
7338
|
+
hook_event_name: "PreToolUse",
|
|
7339
|
+
cwd,
|
|
7340
|
+
session_id: "sess-di-null-redirect",
|
|
7341
|
+
tool_name: "Bash",
|
|
7342
|
+
tool_use_id: `tool-di-real-redirect-${index}`,
|
|
7343
|
+
tool_input: { command },
|
|
7344
|
+
},
|
|
7345
|
+
{ cwd },
|
|
7346
|
+
);
|
|
7347
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
|
|
7348
|
+
}
|
|
7349
|
+
} finally {
|
|
7350
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7351
|
+
}
|
|
7352
|
+
});
|
|
7353
|
+
|
|
7354
|
+
it("allows implementation tools after an explicit deep-interview handoff deactivates the mode", async () => {
|
|
7355
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-handoff-"));
|
|
7356
|
+
try {
|
|
7357
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7358
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-handoff");
|
|
7359
|
+
await mkdir(sessionDir, { recursive: true });
|
|
7360
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
7361
|
+
version: 1,
|
|
7362
|
+
active: true,
|
|
7363
|
+
skill: "deep-interview",
|
|
7364
|
+
phase: "planning",
|
|
6545
7365
|
session_id: "sess-di-handoff",
|
|
6546
7366
|
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-handoff" }],
|
|
6547
7367
|
});
|
|
@@ -6630,6 +7450,207 @@ exit 0
|
|
|
6630
7450
|
}
|
|
6631
7451
|
});
|
|
6632
7452
|
|
|
7453
|
+
it("records native subagent capacity exhaustion from spawn_agent PostToolUse output", async () => {
|
|
7454
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-record-"));
|
|
7455
|
+
try {
|
|
7456
|
+
const result = await dispatchCodexNativeHook(
|
|
7457
|
+
{
|
|
7458
|
+
hook_event_name: "PostToolUse",
|
|
7459
|
+
cwd,
|
|
7460
|
+
session_id: "sess-subagent-capacity-record",
|
|
7461
|
+
thread_id: "thread-subagent-capacity-record",
|
|
7462
|
+
turn_id: "turn-subagent-capacity-record",
|
|
7463
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7464
|
+
tool_response: {
|
|
7465
|
+
error: "collab spawn failed: agent thread limit reached",
|
|
7466
|
+
},
|
|
7467
|
+
},
|
|
7468
|
+
{ cwd },
|
|
7469
|
+
);
|
|
7470
|
+
|
|
7471
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
7472
|
+
assert.equal(result.outputJson, null);
|
|
7473
|
+
|
|
7474
|
+
const blocker = JSON.parse(
|
|
7475
|
+
await readFile(join(cwd, ".omx", "state", "native-subagent-capacity-blocker.json"), "utf-8"),
|
|
7476
|
+
) as Record<string, unknown>;
|
|
7477
|
+
assert.equal(blocker.reason, "agent_thread_limit_reached");
|
|
7478
|
+
assert.equal(blocker.session_id, "sess-subagent-capacity-record");
|
|
7479
|
+
assert.equal(blocker.thread_id, "thread-subagent-capacity-record");
|
|
7480
|
+
assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
|
|
7481
|
+
assert.match(String(blocker.error_summary), /agent thread limit reached/);
|
|
7482
|
+
assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
|
|
7483
|
+
} finally {
|
|
7484
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7485
|
+
}
|
|
7486
|
+
});
|
|
7487
|
+
|
|
7488
|
+
it("blocks close_agent cleanup after recent native subagent capacity exhaustion", async () => {
|
|
7489
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-block-"));
|
|
7490
|
+
try {
|
|
7491
|
+
await dispatchCodexNativeHook(
|
|
7492
|
+
{
|
|
7493
|
+
hook_event_name: "PostToolUse",
|
|
7494
|
+
cwd,
|
|
7495
|
+
session_id: "sess-subagent-capacity-close-block",
|
|
7496
|
+
thread_id: "thread-subagent-capacity-close-block",
|
|
7497
|
+
turn_id: "turn-subagent-capacity-close-block",
|
|
7498
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7499
|
+
tool_response: "collab spawn failed: agent thread limit reached",
|
|
7500
|
+
},
|
|
7501
|
+
{ cwd },
|
|
7502
|
+
);
|
|
7503
|
+
|
|
7504
|
+
const result = await dispatchCodexNativeHook(
|
|
7505
|
+
{
|
|
7506
|
+
hook_event_name: "PreToolUse",
|
|
7507
|
+
cwd,
|
|
7508
|
+
session_id: "sess-subagent-capacity-close-block",
|
|
7509
|
+
thread_id: "thread-subagent-capacity-close-block",
|
|
7510
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7511
|
+
tool_input: { target: "019ecc36-stale" },
|
|
7512
|
+
},
|
|
7513
|
+
{ cwd },
|
|
7514
|
+
);
|
|
7515
|
+
|
|
7516
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
7517
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7518
|
+
assert.match(JSON.stringify(result.outputJson), /agent thread limit reached/);
|
|
7519
|
+
assert.match(JSON.stringify(result.outputJson), /multi_agent_v1\.close_agent/);
|
|
7520
|
+
assert.match(JSON.stringify(result.outputJson), /multi_tool_use\.parallel/);
|
|
7521
|
+
assert.match(JSON.stringify(result.outputJson), /bounded capacity blocker/);
|
|
7522
|
+
} finally {
|
|
7523
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7524
|
+
}
|
|
7525
|
+
});
|
|
7526
|
+
|
|
7527
|
+
it("emits schema-safe PreToolUse CLI stdout for close_agent capacity blocks", async () => {
|
|
7528
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-subagent-capacity-close-block-"));
|
|
7529
|
+
try {
|
|
7530
|
+
parseSingleJsonStdout(runNativeHookCli({
|
|
7531
|
+
hook_event_name: "PostToolUse",
|
|
7532
|
+
cwd,
|
|
7533
|
+
session_id: "sess-cli-subagent-capacity-close-block",
|
|
7534
|
+
thread_id: "thread-cli-subagent-capacity-close-block",
|
|
7535
|
+
turn_id: "turn-cli-subagent-capacity-close-block",
|
|
7536
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7537
|
+
tool_response: "collab spawn failed: agent thread limit reached",
|
|
7538
|
+
}, { cwd }));
|
|
7539
|
+
|
|
7540
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
7541
|
+
hook_event_name: "PreToolUse",
|
|
7542
|
+
cwd,
|
|
7543
|
+
session_id: "sess-cli-subagent-capacity-close-block",
|
|
7544
|
+
thread_id: "thread-cli-subagent-capacity-close-block",
|
|
7545
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7546
|
+
tool_input: { target: "019ecc36-stale" },
|
|
7547
|
+
}, { cwd }));
|
|
7548
|
+
|
|
7549
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
7550
|
+
assert.match(String(output.systemMessage ?? ""), /agent thread limit reached/);
|
|
7551
|
+
assert.match(String(output.systemMessage ?? ""), /Do not call multi_agent_v1\.close_agent/);
|
|
7552
|
+
assert.equal(output.decision, undefined);
|
|
7553
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
7554
|
+
} finally {
|
|
7555
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7556
|
+
}
|
|
7557
|
+
});
|
|
7558
|
+
|
|
7559
|
+
it("blocks parallel close_agent cleanup after recent native subagent capacity exhaustion", async () => {
|
|
7560
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-parallel-close-block-"));
|
|
7561
|
+
try {
|
|
7562
|
+
await dispatchCodexNativeHook(
|
|
7563
|
+
{
|
|
7564
|
+
hook_event_name: "PostToolUse",
|
|
7565
|
+
cwd,
|
|
7566
|
+
session_id: "sess-subagent-capacity-parallel-close-block",
|
|
7567
|
+
thread_id: "thread-subagent-capacity-parallel-close-block",
|
|
7568
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7569
|
+
tool_response: "agent thread limit reached",
|
|
7570
|
+
},
|
|
7571
|
+
{ cwd },
|
|
7572
|
+
);
|
|
7573
|
+
|
|
7574
|
+
const result = await dispatchCodexNativeHook(
|
|
7575
|
+
{
|
|
7576
|
+
hook_event_name: "PreToolUse",
|
|
7577
|
+
cwd,
|
|
7578
|
+
session_id: "sess-subagent-capacity-parallel-close-block",
|
|
7579
|
+
thread_id: "thread-subagent-capacity-parallel-close-block",
|
|
7580
|
+
tool_name: "multi_tool_use.parallel",
|
|
7581
|
+
tool_input: {
|
|
7582
|
+
tool_uses: [
|
|
7583
|
+
{ recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-1" } },
|
|
7584
|
+
{ recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-2" } },
|
|
7585
|
+
],
|
|
7586
|
+
},
|
|
7587
|
+
},
|
|
7588
|
+
{ cwd },
|
|
7589
|
+
);
|
|
7590
|
+
|
|
7591
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7592
|
+
assert.match(JSON.stringify(result.outputJson), /Do not call multi_agent_v1\.close_agent/);
|
|
7593
|
+
assert.match(JSON.stringify(result.outputJson), /do not batch close_agent through multi_tool_use\.parallel/);
|
|
7594
|
+
} finally {
|
|
7595
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7596
|
+
}
|
|
7597
|
+
});
|
|
7598
|
+
|
|
7599
|
+
it("does not block close_agent without a recent native capacity blocker", async () => {
|
|
7600
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-no-blocker-"));
|
|
7601
|
+
try {
|
|
7602
|
+
const result = await dispatchCodexNativeHook(
|
|
7603
|
+
{
|
|
7604
|
+
hook_event_name: "PreToolUse",
|
|
7605
|
+
cwd,
|
|
7606
|
+
session_id: "sess-subagent-capacity-close-no-blocker",
|
|
7607
|
+
thread_id: "thread-subagent-capacity-close-no-blocker",
|
|
7608
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7609
|
+
tool_input: { target: "completed-agent" },
|
|
7610
|
+
},
|
|
7611
|
+
{ cwd },
|
|
7612
|
+
);
|
|
7613
|
+
|
|
7614
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
7615
|
+
assert.equal(result.outputJson, null);
|
|
7616
|
+
} finally {
|
|
7617
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7618
|
+
}
|
|
7619
|
+
});
|
|
7620
|
+
|
|
7621
|
+
it("does not block close_agent when the native capacity blocker is expired", async () => {
|
|
7622
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-expired-"));
|
|
7623
|
+
try {
|
|
7624
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7625
|
+
await mkdir(stateDir, { recursive: true });
|
|
7626
|
+
await writeJson(join(stateDir, "native-subagent-capacity-blocker.json"), {
|
|
7627
|
+
schema_version: 1,
|
|
7628
|
+
reason: "agent_thread_limit_reached",
|
|
7629
|
+
session_id: "sess-subagent-capacity-close-expired",
|
|
7630
|
+
error_summary: "agent thread limit reached",
|
|
7631
|
+
observed_at: "2026-06-20T00:00:00.000Z",
|
|
7632
|
+
expires_at: "2026-06-20T00:30:00.000Z",
|
|
7633
|
+
cwd,
|
|
7634
|
+
});
|
|
7635
|
+
|
|
7636
|
+
const result = await dispatchCodexNativeHook(
|
|
7637
|
+
{
|
|
7638
|
+
hook_event_name: "PreToolUse",
|
|
7639
|
+
cwd,
|
|
7640
|
+
session_id: "sess-subagent-capacity-close-expired",
|
|
7641
|
+
thread_id: "thread-subagent-capacity-close-expired",
|
|
7642
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7643
|
+
tool_input: { target: "completed-agent" },
|
|
7644
|
+
},
|
|
7645
|
+
{ cwd },
|
|
7646
|
+
);
|
|
7647
|
+
|
|
7648
|
+
assert.equal(result.outputJson, null);
|
|
7649
|
+
} finally {
|
|
7650
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7651
|
+
}
|
|
7652
|
+
});
|
|
7653
|
+
|
|
6633
7654
|
it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
|
|
6634
7655
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
|
|
6635
7656
|
try {
|
|
@@ -11113,158 +12134,439 @@ exit 0
|
|
|
11113
12134
|
{
|
|
11114
12135
|
hook_event_name: "Stop",
|
|
11115
12136
|
cwd,
|
|
11116
|
-
session_id: "sess-stop-team-env-root",
|
|
12137
|
+
session_id: "sess-stop-team-env-root",
|
|
12138
|
+
},
|
|
12139
|
+
{ cwd },
|
|
12140
|
+
);
|
|
12141
|
+
|
|
12142
|
+
assert.equal(result.omxEventName, "stop");
|
|
12143
|
+
assert.deepEqual(result.outputJson, {
|
|
12144
|
+
decision: "block",
|
|
12145
|
+
reason:
|
|
12146
|
+
`OMX team pipeline is still active (env-root-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
12147
|
+
stopReason: "team_team-exec",
|
|
12148
|
+
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
12149
|
+
});
|
|
12150
|
+
} finally {
|
|
12151
|
+
if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
|
|
12152
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
12153
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12154
|
+
}
|
|
12155
|
+
});
|
|
12156
|
+
|
|
12157
|
+
it("blocks Stop from session-scoped team mode when session.json points to another session", async () => {
|
|
12158
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-mismatch-"));
|
|
12159
|
+
try {
|
|
12160
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12161
|
+
await mkdir(join(stateDir, "sessions", "sess-live-team"), { recursive: true });
|
|
12162
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-team" });
|
|
12163
|
+
await writeJson(join(stateDir, "sessions", "sess-live-team", "team-state.json"), {
|
|
12164
|
+
active: true,
|
|
12165
|
+
mode: "team",
|
|
12166
|
+
current_phase: "team-exec",
|
|
12167
|
+
team_name: "session-live-team",
|
|
12168
|
+
});
|
|
12169
|
+
await writeJson(join(stateDir, "team", "session-live-team", "phase.json"), {
|
|
12170
|
+
current_phase: "team-exec",
|
|
12171
|
+
max_fix_attempts: 3,
|
|
12172
|
+
current_fix_attempt: 0,
|
|
12173
|
+
transitions: [],
|
|
12174
|
+
updated_at: new Date().toISOString(),
|
|
12175
|
+
});
|
|
12176
|
+
|
|
12177
|
+
const result = await dispatchCodexNativeHook(
|
|
12178
|
+
{
|
|
12179
|
+
hook_event_name: "Stop",
|
|
12180
|
+
cwd,
|
|
12181
|
+
session_id: "sess-live-team",
|
|
12182
|
+
},
|
|
12183
|
+
{ cwd },
|
|
12184
|
+
);
|
|
12185
|
+
|
|
12186
|
+
assert.equal(result.omxEventName, "stop");
|
|
12187
|
+
assert.deepEqual(result.outputJson, {
|
|
12188
|
+
decision: "block",
|
|
12189
|
+
reason:
|
|
12190
|
+
`OMX team pipeline is still active (session-live-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
12191
|
+
stopReason: "team_team-exec",
|
|
12192
|
+
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
12193
|
+
});
|
|
12194
|
+
} finally {
|
|
12195
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12196
|
+
}
|
|
12197
|
+
});
|
|
12198
|
+
|
|
12199
|
+
it("returns Stop continuation output for active ralplan skill with matching active mode state and without active subagents", async () => {
|
|
12200
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-"));
|
|
12201
|
+
try {
|
|
12202
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12203
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill"), { recursive: true });
|
|
12204
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill" });
|
|
12205
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill", "skill-active-state.json"), {
|
|
12206
|
+
active: true,
|
|
12207
|
+
skill: "ralplan",
|
|
12208
|
+
phase: "planning",
|
|
12209
|
+
});
|
|
12210
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill", "ralplan-state.json"), {
|
|
12211
|
+
active: true,
|
|
12212
|
+
current_phase: "planning",
|
|
12213
|
+
});
|
|
12214
|
+
|
|
12215
|
+
const result = await dispatchCodexNativeHook(
|
|
12216
|
+
{
|
|
12217
|
+
hook_event_name: "Stop",
|
|
12218
|
+
cwd,
|
|
12219
|
+
session_id: "sess-stop-skill",
|
|
12220
|
+
},
|
|
12221
|
+
{ cwd },
|
|
12222
|
+
);
|
|
12223
|
+
|
|
12224
|
+
assert.equal(result.omxEventName, "stop");
|
|
12225
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
12226
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
12227
|
+
assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
|
|
12228
|
+
assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
12229
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
12230
|
+
assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
|
|
12231
|
+
} finally {
|
|
12232
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12233
|
+
}
|
|
12234
|
+
});
|
|
12235
|
+
|
|
12236
|
+
it("does not block Stop after owner-session ralplan CLI completion writes to native canonical state", async () => {
|
|
12237
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-owner-alias-complete-"));
|
|
12238
|
+
const previousSessionId = process.env.OMX_SESSION_ID;
|
|
12239
|
+
try {
|
|
12240
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12241
|
+
const nativeSessionId = "native-id";
|
|
12242
|
+
const ownerSessionId = "omx-owner-id";
|
|
12243
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
12244
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
12245
|
+
session_id: nativeSessionId,
|
|
12246
|
+
native_session_id: nativeSessionId,
|
|
12247
|
+
owner_omx_session_id: ownerSessionId,
|
|
12248
|
+
cwd,
|
|
12249
|
+
});
|
|
12250
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
12251
|
+
active: true,
|
|
12252
|
+
skill: "ralplan",
|
|
12253
|
+
phase: "planning",
|
|
12254
|
+
session_id: nativeSessionId,
|
|
12255
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: nativeSessionId }],
|
|
12256
|
+
});
|
|
12257
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralplan-state.json"), {
|
|
12258
|
+
active: true,
|
|
12259
|
+
mode: "ralplan",
|
|
12260
|
+
current_phase: "planning",
|
|
12261
|
+
session_id: nativeSessionId,
|
|
12262
|
+
});
|
|
12263
|
+
process.env.OMX_SESSION_ID = ownerSessionId;
|
|
12264
|
+
|
|
12265
|
+
const writeResult = await executeStateOperation("state_write", {
|
|
12266
|
+
mode: "ralplan",
|
|
12267
|
+
active: false,
|
|
12268
|
+
current_phase: "complete",
|
|
12269
|
+
status: "complete",
|
|
12270
|
+
terminal_state: "complete",
|
|
12271
|
+
workingDirectory: cwd,
|
|
12272
|
+
});
|
|
12273
|
+
|
|
12274
|
+
assert.notEqual(writeResult.isError, true);
|
|
12275
|
+
assert.equal(existsSync(join(stateDir, "sessions", ownerSessionId, "ralplan-state.json")), false);
|
|
12276
|
+
const result = await dispatchCodexNativeHook(
|
|
12277
|
+
{
|
|
12278
|
+
hook_event_name: "Stop",
|
|
12279
|
+
cwd,
|
|
12280
|
+
session_id: nativeSessionId,
|
|
12281
|
+
},
|
|
12282
|
+
{ cwd },
|
|
12283
|
+
);
|
|
12284
|
+
|
|
12285
|
+
assert.equal(result.omxEventName, "stop");
|
|
12286
|
+
assert.equal(result.outputJson, null);
|
|
12287
|
+
} finally {
|
|
12288
|
+
if (typeof previousSessionId === "string") process.env.OMX_SESSION_ID = previousSessionId;
|
|
12289
|
+
else delete process.env.OMX_SESSION_ID;
|
|
12290
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12291
|
+
}
|
|
12292
|
+
});
|
|
12293
|
+
|
|
12294
|
+
it("does not block on stale ralplan skill-active state when the matching mode state is absent", async () => {
|
|
12295
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-skill-"));
|
|
12296
|
+
try {
|
|
12297
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12298
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-stale-skill"), { recursive: true });
|
|
12299
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-stale-skill" });
|
|
12300
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-stale-skill", "skill-active-state.json"), {
|
|
12301
|
+
active: true,
|
|
12302
|
+
skill: "ralplan",
|
|
12303
|
+
phase: "planning",
|
|
12304
|
+
session_id: "sess-stop-stale-skill",
|
|
12305
|
+
active_skills: [{
|
|
12306
|
+
skill: "ralplan",
|
|
12307
|
+
phase: "planning",
|
|
12308
|
+
active: true,
|
|
12309
|
+
session_id: "sess-stop-stale-skill",
|
|
12310
|
+
}],
|
|
12311
|
+
});
|
|
12312
|
+
|
|
12313
|
+
const result = await dispatchCodexNativeHook(
|
|
12314
|
+
{
|
|
12315
|
+
hook_event_name: "Stop",
|
|
12316
|
+
cwd,
|
|
12317
|
+
session_id: "sess-stop-stale-skill",
|
|
12318
|
+
},
|
|
12319
|
+
{ cwd },
|
|
12320
|
+
);
|
|
12321
|
+
|
|
12322
|
+
assert.equal(result.omxEventName, "stop");
|
|
12323
|
+
assert.equal(result.outputJson, null);
|
|
12324
|
+
} finally {
|
|
12325
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12326
|
+
}
|
|
12327
|
+
});
|
|
12328
|
+
|
|
12329
|
+
it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
|
|
12330
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
|
|
12331
|
+
try {
|
|
12332
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12333
|
+
const sessionId = "sess-stop-stale-session-ralplan";
|
|
12334
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12335
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12336
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
12337
|
+
active: false,
|
|
12338
|
+
skill: "ralplan",
|
|
12339
|
+
phase: "reviewing",
|
|
12340
|
+
active_skills: [],
|
|
12341
|
+
});
|
|
12342
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
12343
|
+
active: false,
|
|
12344
|
+
mode: "ralplan",
|
|
12345
|
+
current_phase: "complete",
|
|
12346
|
+
session_id: sessionId,
|
|
12347
|
+
cwd,
|
|
12348
|
+
});
|
|
12349
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
12350
|
+
active: true,
|
|
12351
|
+
skill: "ralplan",
|
|
12352
|
+
phase: "planning",
|
|
12353
|
+
session_id: sessionId,
|
|
12354
|
+
active_skills: [{
|
|
12355
|
+
skill: "ralplan",
|
|
12356
|
+
phase: "planning",
|
|
12357
|
+
active: true,
|
|
12358
|
+
session_id: sessionId,
|
|
12359
|
+
}],
|
|
12360
|
+
});
|
|
12361
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12362
|
+
active: true,
|
|
12363
|
+
mode: "ralplan",
|
|
12364
|
+
current_phase: "planning",
|
|
12365
|
+
session_id: sessionId,
|
|
12366
|
+
});
|
|
12367
|
+
|
|
12368
|
+
const result = await dispatchCodexNativeHook(
|
|
12369
|
+
{
|
|
12370
|
+
hook_event_name: "Stop",
|
|
12371
|
+
cwd,
|
|
12372
|
+
session_id: sessionId,
|
|
11117
12373
|
},
|
|
11118
12374
|
{ cwd },
|
|
11119
12375
|
);
|
|
11120
12376
|
|
|
11121
12377
|
assert.equal(result.omxEventName, "stop");
|
|
11122
|
-
assert.
|
|
11123
|
-
decision: "block",
|
|
11124
|
-
reason:
|
|
11125
|
-
`OMX team pipeline is still active (env-root-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
11126
|
-
stopReason: "team_team-exec",
|
|
11127
|
-
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
11128
|
-
});
|
|
12378
|
+
assert.equal(result.outputJson, null);
|
|
11129
12379
|
} finally {
|
|
11130
|
-
if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
|
|
11131
|
-
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
11132
12380
|
await rm(cwd, { recursive: true, force: true });
|
|
11133
12381
|
}
|
|
11134
12382
|
});
|
|
11135
12383
|
|
|
11136
|
-
it("
|
|
11137
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
12384
|
+
it("keeps blocking current session ralplan when canonical root inactive ralplan state lacks project context", async () => {
|
|
12385
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-canonical-root-no-cwd-"));
|
|
11138
12386
|
try {
|
|
11139
12387
|
const stateDir = join(cwd, ".omx", "state");
|
|
11140
|
-
|
|
11141
|
-
await
|
|
11142
|
-
await writeJson(join(stateDir, "
|
|
12388
|
+
const sessionId = "sess-stop-ralplan-canonical-root-no-cwd";
|
|
12389
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12390
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12391
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
12392
|
+
active: false,
|
|
12393
|
+
skill: "ralplan",
|
|
12394
|
+
phase: "complete",
|
|
12395
|
+
session_id: sessionId,
|
|
12396
|
+
active_skills: [],
|
|
12397
|
+
});
|
|
12398
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
12399
|
+
active: false,
|
|
12400
|
+
mode: "ralplan",
|
|
12401
|
+
current_phase: "complete",
|
|
12402
|
+
session_id: sessionId,
|
|
12403
|
+
});
|
|
12404
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11143
12405
|
active: true,
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
12406
|
+
skill: "ralplan",
|
|
12407
|
+
phase: "planning",
|
|
12408
|
+
session_id: sessionId,
|
|
12409
|
+
active_skills: [{
|
|
12410
|
+
skill: "ralplan",
|
|
12411
|
+
phase: "planning",
|
|
12412
|
+
active: true,
|
|
12413
|
+
session_id: sessionId,
|
|
12414
|
+
}],
|
|
11147
12415
|
});
|
|
11148
|
-
await writeJson(join(stateDir, "
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
12416
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12417
|
+
active: true,
|
|
12418
|
+
mode: "ralplan",
|
|
12419
|
+
current_phase: "planning",
|
|
12420
|
+
session_id: sessionId,
|
|
12421
|
+
cwd,
|
|
11154
12422
|
});
|
|
11155
12423
|
|
|
11156
12424
|
const result = await dispatchCodexNativeHook(
|
|
11157
12425
|
{
|
|
11158
12426
|
hook_event_name: "Stop",
|
|
11159
12427
|
cwd,
|
|
11160
|
-
session_id:
|
|
12428
|
+
session_id: sessionId,
|
|
11161
12429
|
},
|
|
11162
12430
|
{ cwd },
|
|
11163
12431
|
);
|
|
11164
12432
|
|
|
11165
12433
|
assert.equal(result.omxEventName, "stop");
|
|
11166
|
-
assert.
|
|
11167
|
-
|
|
11168
|
-
reason:
|
|
11169
|
-
`OMX team pipeline is still active (session-live-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
11170
|
-
stopReason: "team_team-exec",
|
|
11171
|
-
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
11172
|
-
});
|
|
12434
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
12435
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
11173
12436
|
} finally {
|
|
11174
12437
|
await rm(cwd, { recursive: true, force: true });
|
|
11175
12438
|
}
|
|
11176
12439
|
});
|
|
11177
12440
|
|
|
11178
|
-
it("
|
|
11179
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
12441
|
+
it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
|
|
12442
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
|
|
11180
12443
|
try {
|
|
11181
12444
|
const stateDir = join(cwd, ".omx", "state");
|
|
11182
|
-
|
|
11183
|
-
await
|
|
11184
|
-
await writeJson(join(stateDir, "
|
|
12445
|
+
const sessionId = "sess-stop-current-active-ralplan";
|
|
12446
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12447
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12448
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
12449
|
+
active: false,
|
|
12450
|
+
skill: "ralplan",
|
|
12451
|
+
phase: "complete",
|
|
12452
|
+
session_id: "sess-stop-old-ralplan",
|
|
12453
|
+
active_skills: [],
|
|
12454
|
+
});
|
|
12455
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
12456
|
+
active: false,
|
|
12457
|
+
mode: "ralplan",
|
|
12458
|
+
current_phase: "complete",
|
|
12459
|
+
session_id: "sess-stop-old-ralplan",
|
|
12460
|
+
});
|
|
12461
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11185
12462
|
active: true,
|
|
11186
12463
|
skill: "ralplan",
|
|
11187
12464
|
phase: "planning",
|
|
12465
|
+
session_id: sessionId,
|
|
12466
|
+
active_skills: [{
|
|
12467
|
+
skill: "ralplan",
|
|
12468
|
+
phase: "planning",
|
|
12469
|
+
active: true,
|
|
12470
|
+
session_id: sessionId,
|
|
12471
|
+
}],
|
|
11188
12472
|
});
|
|
11189
|
-
await writeJson(join(stateDir, "sessions",
|
|
12473
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11190
12474
|
active: true,
|
|
12475
|
+
mode: "ralplan",
|
|
11191
12476
|
current_phase: "planning",
|
|
12477
|
+
session_id: sessionId,
|
|
11192
12478
|
});
|
|
11193
12479
|
|
|
11194
12480
|
const result = await dispatchCodexNativeHook(
|
|
11195
12481
|
{
|
|
11196
12482
|
hook_event_name: "Stop",
|
|
11197
12483
|
cwd,
|
|
11198
|
-
session_id:
|
|
12484
|
+
session_id: sessionId,
|
|
11199
12485
|
},
|
|
11200
12486
|
{ cwd },
|
|
11201
12487
|
);
|
|
11202
12488
|
|
|
11203
12489
|
assert.equal(result.omxEventName, "stop");
|
|
11204
12490
|
assert.equal(result.outputJson?.decision, "block");
|
|
11205
|
-
assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
11206
|
-
assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
|
|
11207
|
-
assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
11208
12491
|
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
11209
|
-
assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
|
|
11210
12492
|
} finally {
|
|
11211
12493
|
await rm(cwd, { recursive: true, force: true });
|
|
11212
12494
|
}
|
|
11213
12495
|
});
|
|
11214
12496
|
|
|
11215
|
-
it("
|
|
11216
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
12497
|
+
it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
|
|
12498
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
|
|
11217
12499
|
try {
|
|
11218
12500
|
const stateDir = join(cwd, ".omx", "state");
|
|
11219
|
-
|
|
11220
|
-
await
|
|
11221
|
-
await writeJson(join(stateDir, "
|
|
12501
|
+
const sessionId = "sess-stop-unscoped-root-current-active";
|
|
12502
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12503
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12504
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
12505
|
+
active: false,
|
|
12506
|
+
skill: "ralplan",
|
|
12507
|
+
phase: "complete",
|
|
12508
|
+
active_skills: [],
|
|
12509
|
+
});
|
|
12510
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
12511
|
+
active: false,
|
|
12512
|
+
mode: "ralplan",
|
|
12513
|
+
current_phase: "complete",
|
|
12514
|
+
});
|
|
12515
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11222
12516
|
active: true,
|
|
11223
12517
|
skill: "ralplan",
|
|
11224
12518
|
phase: "planning",
|
|
11225
|
-
session_id:
|
|
12519
|
+
session_id: sessionId,
|
|
11226
12520
|
active_skills: [{
|
|
11227
12521
|
skill: "ralplan",
|
|
11228
12522
|
phase: "planning",
|
|
11229
12523
|
active: true,
|
|
11230
|
-
session_id:
|
|
12524
|
+
session_id: sessionId,
|
|
11231
12525
|
}],
|
|
11232
12526
|
});
|
|
12527
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12528
|
+
active: true,
|
|
12529
|
+
mode: "ralplan",
|
|
12530
|
+
current_phase: "planning",
|
|
12531
|
+
session_id: sessionId,
|
|
12532
|
+
});
|
|
11233
12533
|
|
|
11234
12534
|
const result = await dispatchCodexNativeHook(
|
|
11235
12535
|
{
|
|
11236
12536
|
hook_event_name: "Stop",
|
|
11237
12537
|
cwd,
|
|
11238
|
-
session_id:
|
|
12538
|
+
session_id: sessionId,
|
|
11239
12539
|
},
|
|
11240
12540
|
{ cwd },
|
|
11241
12541
|
);
|
|
11242
12542
|
|
|
11243
12543
|
assert.equal(result.omxEventName, "stop");
|
|
11244
|
-
assert.equal(result.outputJson,
|
|
12544
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
12545
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
11245
12546
|
} finally {
|
|
11246
12547
|
await rm(cwd, { recursive: true, force: true });
|
|
11247
12548
|
}
|
|
11248
12549
|
});
|
|
11249
12550
|
|
|
11250
|
-
it("
|
|
11251
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
12551
|
+
it("keeps blocking current session ralplan when unscoped root completion lacks a plan artifact", async () => {
|
|
12552
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-no-plan-artifact-"));
|
|
11252
12553
|
try {
|
|
11253
12554
|
const stateDir = join(cwd, ".omx", "state");
|
|
11254
|
-
const sessionId = "sess-stop-
|
|
12555
|
+
const sessionId = "sess-stop-unscoped-root-no-plan-artifact";
|
|
11255
12556
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11256
12557
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11257
12558
|
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
11258
12559
|
active: false,
|
|
11259
12560
|
skill: "ralplan",
|
|
11260
|
-
phase: "
|
|
12561
|
+
phase: "complete",
|
|
11261
12562
|
active_skills: [],
|
|
11262
12563
|
});
|
|
11263
12564
|
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
11264
12565
|
active: false,
|
|
11265
12566
|
mode: "ralplan",
|
|
11266
12567
|
current_phase: "complete",
|
|
11267
|
-
|
|
12568
|
+
planning_complete: true,
|
|
12569
|
+
updated_at: "2026-06-21T08:05:00.000Z",
|
|
11268
12570
|
});
|
|
11269
12571
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11270
12572
|
active: true,
|
|
@@ -11283,6 +12585,7 @@ exit 0
|
|
|
11283
12585
|
mode: "ralplan",
|
|
11284
12586
|
current_phase: "planning",
|
|
11285
12587
|
session_id: sessionId,
|
|
12588
|
+
updated_at: "2026-06-21T08:00:00.000Z",
|
|
11286
12589
|
});
|
|
11287
12590
|
|
|
11288
12591
|
const result = await dispatchCodexNativeHook(
|
|
@@ -11295,32 +12598,83 @@ exit 0
|
|
|
11295
12598
|
);
|
|
11296
12599
|
|
|
11297
12600
|
assert.equal(result.omxEventName, "stop");
|
|
11298
|
-
assert.equal(result.outputJson,
|
|
12601
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
12602
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
11299
12603
|
} finally {
|
|
11300
12604
|
await rm(cwd, { recursive: true, force: true });
|
|
11301
12605
|
}
|
|
11302
12606
|
});
|
|
11303
12607
|
|
|
11304
|
-
it("
|
|
11305
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-
|
|
12608
|
+
it("does not loop when newer unscoped root ralplan complete state shadows stale session planning", async () => {
|
|
12609
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-newer-root-complete-"));
|
|
11306
12610
|
try {
|
|
11307
12611
|
const stateDir = join(cwd, ".omx", "state");
|
|
11308
|
-
const sessionId = "sess-stop-
|
|
12612
|
+
const sessionId = "sess-stop-stale-session-ralplan-newer-root";
|
|
11309
12613
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11310
12614
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12615
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
12616
|
+
active: true,
|
|
12617
|
+
skill: "ralplan",
|
|
12618
|
+
phase: "planning",
|
|
12619
|
+
session_id: sessionId,
|
|
12620
|
+
active_skills: [{
|
|
12621
|
+
skill: "ralplan",
|
|
12622
|
+
phase: "planning",
|
|
12623
|
+
active: true,
|
|
12624
|
+
session_id: sessionId,
|
|
12625
|
+
}],
|
|
12626
|
+
});
|
|
12627
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12628
|
+
active: true,
|
|
12629
|
+
mode: "ralplan",
|
|
12630
|
+
current_phase: "planning",
|
|
12631
|
+
session_id: sessionId,
|
|
12632
|
+
updated_at: "2026-06-21T08:00:00.000Z",
|
|
12633
|
+
});
|
|
12634
|
+
await mkdir(join(cwd, ".omx", "plans"), { recursive: true });
|
|
12635
|
+
await writeFile(join(cwd, ".omx", "plans", "prd-issue-2923.md"), "# PRD\n");
|
|
12636
|
+
await writeFile(join(cwd, ".omx", "plans", "test-spec-issue-2923.md"), "# Test spec\n");
|
|
11311
12637
|
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
11312
12638
|
active: false,
|
|
11313
12639
|
skill: "ralplan",
|
|
11314
12640
|
phase: "complete",
|
|
11315
|
-
session_id: "sess-stop-old-ralplan",
|
|
11316
12641
|
active_skills: [],
|
|
11317
12642
|
});
|
|
11318
12643
|
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
11319
12644
|
active: false,
|
|
11320
12645
|
mode: "ralplan",
|
|
11321
12646
|
current_phase: "complete",
|
|
11322
|
-
|
|
12647
|
+
cwd,
|
|
12648
|
+
planning_complete: true,
|
|
12649
|
+
latest_plan_path: ".omx/plans/prd-issue-2923.md",
|
|
12650
|
+
updated_at: "2026-06-21T08:05:00.000Z",
|
|
11323
12651
|
});
|
|
12652
|
+
|
|
12653
|
+
const result = await dispatchCodexNativeHook(
|
|
12654
|
+
{
|
|
12655
|
+
hook_event_name: "Stop",
|
|
12656
|
+
cwd,
|
|
12657
|
+
session_id: sessionId,
|
|
12658
|
+
tool_name: "apply_patch",
|
|
12659
|
+
},
|
|
12660
|
+
{ cwd },
|
|
12661
|
+
);
|
|
12662
|
+
|
|
12663
|
+
assert.equal(result.omxEventName, "stop");
|
|
12664
|
+
assert.equal(result.outputJson, null);
|
|
12665
|
+
} finally {
|
|
12666
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12667
|
+
}
|
|
12668
|
+
});
|
|
12669
|
+
|
|
12670
|
+
it("keeps blocking current session ralplan when newer unscoped root completion belongs to another worktree", async () => {
|
|
12671
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-cross-worktree-root-complete-"));
|
|
12672
|
+
try {
|
|
12673
|
+
const otherWorktree = join(tmpdir(), "omx-native-hook-other-worktree-2925");
|
|
12674
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12675
|
+
const sessionId = "sess-stop-ralplan-cross-worktree-root";
|
|
12676
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12677
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11324
12678
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11325
12679
|
active: true,
|
|
11326
12680
|
skill: "ralplan",
|
|
@@ -11338,6 +12692,23 @@ exit 0
|
|
|
11338
12692
|
mode: "ralplan",
|
|
11339
12693
|
current_phase: "planning",
|
|
11340
12694
|
session_id: sessionId,
|
|
12695
|
+
cwd,
|
|
12696
|
+
updated_at: "2026-06-21T08:00:00.000Z",
|
|
12697
|
+
});
|
|
12698
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
12699
|
+
active: false,
|
|
12700
|
+
skill: "ralplan",
|
|
12701
|
+
phase: "complete",
|
|
12702
|
+
active_skills: [],
|
|
12703
|
+
});
|
|
12704
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
12705
|
+
active: false,
|
|
12706
|
+
mode: "ralplan",
|
|
12707
|
+
current_phase: "complete",
|
|
12708
|
+
cwd: otherWorktree,
|
|
12709
|
+
planning_complete: true,
|
|
12710
|
+
latest_plan_path: ".omx/plans/prd-issue-2925.md",
|
|
12711
|
+
updated_at: "2026-06-21T08:05:00.000Z",
|
|
11341
12712
|
});
|
|
11342
12713
|
|
|
11343
12714
|
const result = await dispatchCodexNativeHook(
|
|
@@ -11345,6 +12716,7 @@ exit 0
|
|
|
11345
12716
|
hook_event_name: "Stop",
|
|
11346
12717
|
cwd,
|
|
11347
12718
|
session_id: sessionId,
|
|
12719
|
+
tool_name: "apply_patch",
|
|
11348
12720
|
},
|
|
11349
12721
|
{ cwd },
|
|
11350
12722
|
);
|
|
@@ -11357,24 +12729,13 @@ exit 0
|
|
|
11357
12729
|
}
|
|
11358
12730
|
});
|
|
11359
12731
|
|
|
11360
|
-
it("keeps blocking current session ralplan when root
|
|
11361
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
12732
|
+
it("keeps blocking current session ralplan when newer unscoped root completion lacks project context", async () => {
|
|
12733
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralplan-root-complete-no-cwd-"));
|
|
11362
12734
|
try {
|
|
11363
12735
|
const stateDir = join(cwd, ".omx", "state");
|
|
11364
|
-
const sessionId = "sess-stop-
|
|
12736
|
+
const sessionId = "sess-stop-ralplan-root-complete-no-cwd";
|
|
11365
12737
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11366
12738
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11367
|
-
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
11368
|
-
active: false,
|
|
11369
|
-
skill: "ralplan",
|
|
11370
|
-
phase: "complete",
|
|
11371
|
-
active_skills: [],
|
|
11372
|
-
});
|
|
11373
|
-
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
11374
|
-
active: false,
|
|
11375
|
-
mode: "ralplan",
|
|
11376
|
-
current_phase: "complete",
|
|
11377
|
-
});
|
|
11378
12739
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11379
12740
|
active: true,
|
|
11380
12741
|
skill: "ralplan",
|
|
@@ -11392,6 +12753,22 @@ exit 0
|
|
|
11392
12753
|
mode: "ralplan",
|
|
11393
12754
|
current_phase: "planning",
|
|
11394
12755
|
session_id: sessionId,
|
|
12756
|
+
cwd,
|
|
12757
|
+
updated_at: "2026-06-21T08:00:00.000Z",
|
|
12758
|
+
});
|
|
12759
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
12760
|
+
active: false,
|
|
12761
|
+
skill: "ralplan",
|
|
12762
|
+
phase: "complete",
|
|
12763
|
+
active_skills: [],
|
|
12764
|
+
});
|
|
12765
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
12766
|
+
active: false,
|
|
12767
|
+
mode: "ralplan",
|
|
12768
|
+
current_phase: "complete",
|
|
12769
|
+
planning_complete: true,
|
|
12770
|
+
latest_plan_path: ".omx/plans/prd-issue-2925.md",
|
|
12771
|
+
updated_at: "2026-06-21T08:05:00.000Z",
|
|
11395
12772
|
});
|
|
11396
12773
|
|
|
11397
12774
|
const result = await dispatchCodexNativeHook(
|
|
@@ -11399,6 +12776,7 @@ exit 0
|
|
|
11399
12776
|
hook_event_name: "Stop",
|
|
11400
12777
|
cwd,
|
|
11401
12778
|
session_id: sessionId,
|
|
12779
|
+
tool_name: "apply_patch",
|
|
11402
12780
|
},
|
|
11403
12781
|
{ cwd },
|
|
11404
12782
|
);
|
|
@@ -11435,6 +12813,7 @@ exit 0
|
|
|
11435
12813
|
mode: "ralplan",
|
|
11436
12814
|
current_phase: "complete",
|
|
11437
12815
|
session_id: sessionId,
|
|
12816
|
+
cwd,
|
|
11438
12817
|
});
|
|
11439
12818
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11440
12819
|
active: true,
|