oh-my-codex 0.18.6 → 0.18.8
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 +59 -10
- package/crates/omx-sparkshell/tests/execution.rs +1 -1
- package/dist/agents/__tests__/definitions.test.js +11 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +56 -6
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +10 -0
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +5 -1
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.d.ts +5 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +19 -4
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
- package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/fsm.test.js +75 -0
- package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
- package/dist/autopilot/deep-interview-gate.d.ts +18 -0
- package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
- package/dist/autopilot/deep-interview-gate.js +256 -0
- package/dist/autopilot/deep-interview-gate.js.map +1 -0
- package/dist/autopilot/fsm.d.ts +13 -0
- package/dist/autopilot/fsm.d.ts.map +1 -0
- package/dist/autopilot/fsm.js +70 -0
- package/dist/autopilot/fsm.js.map +1 -0
- package/dist/autopilot/ralplan-gate.d.ts +17 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
- package/dist/autopilot/ralplan-gate.js +61 -0
- package/dist/autopilot/ralplan-gate.js.map +1 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +83 -7
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +175 -6
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +8 -4
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +100 -0
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +14 -0
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +83 -0
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +21 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +2 -2
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +110 -2
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +8 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +14 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +298 -50
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +14 -2
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +62 -15
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/question.d.ts.map +1 -1
- package/dist/cli/question.js +36 -5
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -1
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +2 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +4 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +166 -27
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +8 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/tmux-hook.d.ts.map +1 -1
- package/dist/cli/tmux-hook.js +16 -0
- package/dist/cli/tmux-hook.js.map +1 -1
- package/dist/cli/update.d.ts +2 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +47 -3
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/deep-interview.test.js +7 -6
- package/dist/config/__tests__/deep-interview.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +1 -0
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/deep-interview.d.ts.map +1 -1
- package/dist/config/deep-interview.js +14 -4
- package/dist/config/deep-interview.js.map +1 -1
- package/dist/config/generator.d.ts +2 -2
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +2 -2
- package/dist/config/generator.js.map +1 -1
- package/dist/config/team-mode.d.ts +12 -0
- package/dist/config/team-mode.d.ts.map +1 -0
- package/dist/config/team-mode.js +91 -0
- package/dist/config/team-mode.js.map +1 -0
- package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +8 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +1072 -14
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +64 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +25 -0
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
- package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +36 -50
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/deep-interview-config-instruction.js +1 -1
- package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +17 -21
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +1 -0
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +428 -32
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +1 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +6 -0
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/session.d.ts +3 -0
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +13 -5
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/authority.test.js +469 -31
- package/dist/hud/__tests__/authority.test.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +210 -2
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +588 -28
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +61 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +208 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +314 -22
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/authority.d.ts +5 -0
- package/dist/hud/authority.d.ts.map +1 -1
- package/dist/hud/authority.js +337 -30
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/index.d.ts +20 -2
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +103 -26
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts +3 -3
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +129 -20
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +35 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +64 -50
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +26 -6
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +173 -38
- package/dist/hud/tmux.js.map +1 -1
- package/dist/hud/types.d.ts +11 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +71 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +13 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts +12 -2
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +83 -9
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +32 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +113 -17
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +130 -0
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.js +1 -1
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts +1 -0
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +14 -5
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/question/__tests__/deep-interview.test.js +160 -2
- package/dist/question/__tests__/deep-interview.test.js.map +1 -1
- package/dist/question/__tests__/policy.test.js +63 -3
- package/dist/question/__tests__/policy.test.js.map +1 -1
- package/dist/question/__tests__/renderer.test.js +191 -2
- package/dist/question/__tests__/renderer.test.js.map +1 -1
- package/dist/question/__tests__/state.test.js +94 -3
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js +4 -0
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.d.ts +12 -2
- package/dist/question/autopilot-wait.d.ts.map +1 -1
- package/dist/question/autopilot-wait.js +158 -47
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/deep-interview.d.ts.map +1 -1
- package/dist/question/deep-interview.js +22 -6
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/policy.d.ts.map +1 -1
- package/dist/question/policy.js +2 -5
- package/dist/question/policy.js.map +1 -1
- package/dist/question/renderer.d.ts +12 -0
- package/dist/question/renderer.d.ts.map +1 -1
- package/dist/question/renderer.js +87 -3
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts +8 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js +54 -14
- package/dist/question/state.js.map +1 -1
- package/dist/question/types.d.ts +1 -1
- package/dist/question/types.d.ts.map +1 -1
- package/dist/question/ui.d.ts +1 -0
- package/dist/question/ui.d.ts.map +1 -1
- package/dist/question/ui.js +1 -0
- package/dist/question/ui.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +191 -0
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +84 -2
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +9 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +32 -11
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +2315 -280
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js +74 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +431 -56
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +79 -1
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/eval/eval-parity-smoke.js +1 -1
- package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts +9 -0
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
- package/dist/scripts/hook-payload-guard.js +111 -0
- package/dist/scripts/hook-payload-guard.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +8 -1
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +3 -1
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +62 -38
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +24 -18
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +86 -13
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/run-test-files.js +193 -22
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +61 -3
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-agents.d.ts.map +1 -1
- package/dist/scripts/verify-native-agents.js +58 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +1125 -1
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +46 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +98 -7
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +159 -2
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.js +6 -8
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +6 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +38 -15
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +10 -3
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +139 -0
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +3 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +41 -4
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
- package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
- package/dist/team/__tests__/coordination-protocol.test.js +173 -0
- package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
- package/dist/team/__tests__/runtime.test.js +52 -3
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +9 -4
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +83 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +240 -2
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/coordination-protocol.d.ts +14 -0
- package/dist/team/coordination-protocol.d.ts.map +1 -0
- package/dist/team/coordination-protocol.js +244 -0
- package/dist/team/coordination-protocol.js.map +1 -0
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +19 -3
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +3 -2
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +24 -0
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +21 -1
- 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 +17 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +12 -5
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +1 -1
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +2 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +161 -13
- 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 +63 -0
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +4 -2
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +3 -0
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +81 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +8 -8
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
- package/plugins/oh-my-codex/hooks/hooks.json +1 -2
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -6
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
- package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
- package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
- package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
- package/skills/autopilot/SKILL.md +13 -6
- package/skills/code-review/SKILL.md +7 -7
- package/skills/deep-interview/SKILL.md +9 -4
- package/skills/ralph/SKILL.md +22 -22
- package/skills/ralplan/SKILL.md +12 -0
- package/skills/team/SKILL.md +16 -0
- package/skills/ultraqa/SKILL.md +9 -0
- package/skills/worker/SKILL.md +14 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +4435 -2083
- package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
- package/src/scripts/__tests__/run-test-files.test.ts +102 -0
- package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
- package/src/scripts/codex-native-hook.ts +536 -51
- package/src/scripts/codex-native-pre-post.ts +80 -0
- package/src/scripts/demo-team-e2e.sh +10 -7
- package/src/scripts/eval/eval-parity-smoke.ts +1 -1
- package/src/scripts/hook-payload-guard.ts +113 -0
- package/src/scripts/notify-fallback-watcher.ts +8 -1
- package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
- package/src/scripts/notify-hook/auto-nudge.ts +3 -1
- package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
- package/src/scripts/notify-hook/state-io.ts +75 -37
- package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
- package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
- package/src/scripts/notify-hook/tmux-injection.ts +35 -19
- package/src/scripts/notify-hook.ts +105 -6
- package/src/scripts/run-test-files.ts +192 -22
- package/src/scripts/sync-plugin-mirror.ts +98 -9
- package/src/scripts/verify-native-agents.ts +65 -1
|
@@ -22,6 +22,8 @@ import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.j
|
|
|
22
22
|
import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
|
|
23
23
|
import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
|
|
24
24
|
import { getBaseStateDir } from "../../state/paths.js";
|
|
25
|
+
import { maybeNudgeLeaderForAllowedWorkerStop } from "../notify-hook/team-worker-stop.js";
|
|
26
|
+
import { MAX_NATIVE_STDIN_JSON_BYTES } from "../hook-payload-guard.js";
|
|
25
27
|
function nativeHookScriptPath() {
|
|
26
28
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
27
29
|
}
|
|
@@ -44,6 +46,23 @@ async function writeJson(path, value) {
|
|
|
44
46
|
await mkdir(dirname(path), { recursive: true }).catch(() => { });
|
|
45
47
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
46
48
|
}
|
|
49
|
+
async function writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId) {
|
|
50
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
51
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
52
|
+
session_id: sessionId,
|
|
53
|
+
native_session_id: nativeSessionId,
|
|
54
|
+
cwd,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function writeSessionSkillActiveState(stateDir, sessionId, skill, phase) {
|
|
58
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
59
|
+
active: true,
|
|
60
|
+
skill,
|
|
61
|
+
phase,
|
|
62
|
+
session_id: sessionId,
|
|
63
|
+
active_skills: [{ skill, phase, active: true, session_id: sessionId }],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
47
66
|
async function setTeamPaneIds(cwd, teamName, paneIds) {
|
|
48
67
|
for (const fileName of ["config.json", "manifest.v2.json"]) {
|
|
49
68
|
const filePath = join(cwd, ".omx", "state", "team", teamName, fileName);
|
|
@@ -104,6 +123,12 @@ async function withLoreGuardConfig(value, prefix, run) {
|
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
|
|
126
|
+
const rawCaptureText = options.captureText ?? (options.busyLeader ? "• Working… (esc to interrupt)" : "› ready");
|
|
127
|
+
const captureText = `'${rawCaptureText.replace(/'/g, "'\"'\"'")}'`;
|
|
128
|
+
const currentCommand = `'${(options.currentCommand ?? "codex").replace(/'/g, "'\"'\"'")}'`;
|
|
129
|
+
const sendDelaySeconds = Math.max(0, options.sendDelayMs ?? 0) / 1000;
|
|
130
|
+
const removePathOnSend = options.removePathOnSend ? `'${options.removePathOnSend.replace(/'/g, "'\"'\"'")}'` : "";
|
|
131
|
+
const removePathOnCapture = options.removePathOnCapture ? `'${options.removePathOnCapture.replace(/'/g, "'\"'\"'")}'` : "";
|
|
107
132
|
return `#!/usr/bin/env bash
|
|
108
133
|
set -eu
|
|
109
134
|
echo "$@" >> "${tmuxLogPath}"
|
|
@@ -124,17 +149,20 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
124
149
|
"#{pane_id}") echo "%42" ;;
|
|
125
150
|
"#{pane_current_path}") pwd ;;
|
|
126
151
|
"#{pane_start_command}") echo "codex" ;;
|
|
127
|
-
"#{pane_current_command}")
|
|
152
|
+
"#{pane_current_command}") printf '%s\\n' ${currentCommand} ;;
|
|
128
153
|
"#S") echo "omx-team-worker-stop" ;;
|
|
129
154
|
*) ;;
|
|
130
155
|
esac
|
|
131
156
|
exit 0
|
|
132
157
|
fi
|
|
133
158
|
if [[ "$cmd" == "capture-pane" ]]; then
|
|
134
|
-
${
|
|
159
|
+
${removePathOnCapture ? `rm -rf ${removePathOnCapture}` : ""}
|
|
160
|
+
printf '%s\\n' ${captureText}
|
|
135
161
|
exit 0
|
|
136
162
|
fi
|
|
137
163
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
164
|
+
${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
|
|
165
|
+
${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
|
|
138
166
|
${options.failSend ? "exit 1" : "exit 0"}
|
|
139
167
|
fi
|
|
140
168
|
exit 0
|
|
@@ -281,13 +309,78 @@ describe("codex native hook dispatch", () => {
|
|
|
281
309
|
it("does not treat a different module url as the main module", () => {
|
|
282
310
|
assert.equal(isCodexNativeHookMainModule(pathToFileURL("/tmp/omx native/other-script.js").href, "/tmp/omx native/codex-native-hook.js"), false);
|
|
283
311
|
});
|
|
284
|
-
it("emits
|
|
312
|
+
it("emits schema-safe JSON stdout when CLI stdin is malformed", () => {
|
|
285
313
|
const stdout = runNativeHookCli("{");
|
|
286
314
|
const output = parseSingleJsonStdout(stdout);
|
|
315
|
+
assert.equal(output.continue, false);
|
|
316
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
317
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
318
|
+
assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
|
|
319
|
+
});
|
|
320
|
+
it("redacts unterminated prompt-like malformed stdin fields", async () => {
|
|
321
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-unterminated-"));
|
|
322
|
+
try {
|
|
323
|
+
const privatePrompt = "PRIVATE_UNTERMINATED_PROMPT";
|
|
324
|
+
const malformed = `{hook_event_name:"PostToolUse", prompt:"${privatePrompt}`;
|
|
325
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
326
|
+
cwd,
|
|
327
|
+
input: malformed,
|
|
328
|
+
encoding: "utf-8",
|
|
329
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
330
|
+
});
|
|
331
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
332
|
+
assert.equal(result.stderr, "");
|
|
333
|
+
const output = parseSingleJsonStdout(result.stdout);
|
|
334
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
335
|
+
const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
|
|
336
|
+
const entry = JSON.parse(log.trim());
|
|
337
|
+
const prefix = String(entry.raw_input_prefix ?? "");
|
|
338
|
+
assert.doesNotMatch(prefix, new RegExp(privatePrompt));
|
|
339
|
+
assert.match(prefix, /prompt:"\[REDACTED\]"/);
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
await rm(cwd, { recursive: true, force: true });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
it("logs a bounded redacted raw stdin prefix when CLI stdin is malformed", async () => {
|
|
346
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-log-prefix-"));
|
|
347
|
+
try {
|
|
348
|
+
const secret = "sk-test-secret123456";
|
|
349
|
+
const promptText = "summarize private launch notes";
|
|
350
|
+
const malformed = `{hook_event_name:"PostToolUse", access_token:"${secret}", prompt:"${promptText}", text:"${promptText}", bad:"${"x".repeat(400)}"}${String.fromCharCode(10, 0, 7)}`;
|
|
351
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
352
|
+
cwd,
|
|
353
|
+
input: malformed,
|
|
354
|
+
encoding: "utf-8",
|
|
355
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
356
|
+
});
|
|
357
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
358
|
+
assert.equal(result.stderr, "");
|
|
359
|
+
const output = parseSingleJsonStdout(result.stdout);
|
|
360
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
361
|
+
const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
|
|
362
|
+
const entry = JSON.parse(log.trim());
|
|
363
|
+
const prefix = String(entry.raw_input_prefix ?? "");
|
|
364
|
+
assert.equal(entry.type, "native_hook_stdin_parse_error");
|
|
365
|
+
assert.equal(entry.raw_input_length, Buffer.byteLength(malformed, "utf-8"));
|
|
366
|
+
assert.ok(prefix.length <= 240, `prefix should be bounded, got ${prefix.length}`);
|
|
367
|
+
assert.doesNotMatch(prefix, /[\u0000-\u001f\u007f-\u009f]/);
|
|
368
|
+
assert.doesNotMatch(prefix, new RegExp(secret));
|
|
369
|
+
assert.doesNotMatch(prefix, new RegExp(promptText));
|
|
370
|
+
assert.match(prefix, /\[REDACTED\]/);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
await rm(cwd, { recursive: true, force: true });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
it("emits Stop-schema-safe block JSON when malformed stdin still identifies Stop", () => {
|
|
377
|
+
const stdout = runNativeHookCli('{hook_event_name:"Stop",');
|
|
378
|
+
const output = parseSingleJsonStdout(stdout);
|
|
287
379
|
assert.equal(output.decision, "block");
|
|
288
380
|
assert.equal(output.reason, "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.");
|
|
289
|
-
assert.equal(output.
|
|
290
|
-
assert.
|
|
381
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
382
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
383
|
+
assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
|
|
291
384
|
});
|
|
292
385
|
it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
|
|
293
386
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
|
|
@@ -301,6 +394,112 @@ describe("codex native hook dispatch", () => {
|
|
|
301
394
|
}, { cwd });
|
|
302
395
|
const output = parseSingleJsonStdout(stdout);
|
|
303
396
|
assert.deepEqual(output, {});
|
|
397
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
await rm(cwd, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
|
|
404
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
|
|
405
|
+
try {
|
|
406
|
+
const oversizedStop = JSON.stringify({
|
|
407
|
+
hook_event_name: "Stop",
|
|
408
|
+
cwd,
|
|
409
|
+
session_id: "sess-cli-stop-oversized",
|
|
410
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
411
|
+
});
|
|
412
|
+
const stdout = runNativeHookCli(oversizedStop, { cwd });
|
|
413
|
+
assert.deepEqual(parseSingleJsonStdout(stdout), {});
|
|
414
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
await rm(cwd, { recursive: true, force: true });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
it("blocks oversized Stop stdin when current session autopilot is active", async () => {
|
|
421
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-active-"));
|
|
422
|
+
try {
|
|
423
|
+
await writeActiveAutopilotSession(cwd, "sess-cli-stop-oversized-active");
|
|
424
|
+
const oversizedStop = JSON.stringify({
|
|
425
|
+
hook_event_name: "Stop",
|
|
426
|
+
cwd,
|
|
427
|
+
session_id: "native-session-hidden-by-oversized-payload",
|
|
428
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
429
|
+
});
|
|
430
|
+
const output = parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd }));
|
|
431
|
+
assert.equal(output.decision, "block");
|
|
432
|
+
assert.equal(output.stopReason, "native_stop_stdin_oversized_active_workflow");
|
|
433
|
+
assert.match(String(output.systemMessage ?? ""), /active current-session workflow state/);
|
|
434
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await rm(cwd, { recursive: true, force: true });
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
it("does not block oversized Stop stdin for unrelated root autopilot state", async () => {
|
|
441
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-stale-root-"));
|
|
442
|
+
try {
|
|
443
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), {
|
|
444
|
+
session_id: "sess-current-without-active-autopilot",
|
|
445
|
+
cwd,
|
|
446
|
+
});
|
|
447
|
+
await writeJson(join(cwd, ".omx", "state", "autopilot-state.json"), {
|
|
448
|
+
active: true,
|
|
449
|
+
current_phase: "execution",
|
|
450
|
+
});
|
|
451
|
+
const oversizedStop = JSON.stringify({
|
|
452
|
+
hook_event_name: "Stop",
|
|
453
|
+
cwd,
|
|
454
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
455
|
+
});
|
|
456
|
+
assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
|
|
457
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
await rm(cwd, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
it("does not block oversized Stop stdin when terminal run-state shadows stale autopilot state", async () => {
|
|
464
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-terminal-run-"));
|
|
465
|
+
try {
|
|
466
|
+
const sessionId = "sess-cli-stop-oversized-terminal-run";
|
|
467
|
+
await writeActiveAutopilotSession(cwd, sessionId);
|
|
468
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "run-state.json"), {
|
|
469
|
+
version: 1,
|
|
470
|
+
active: false,
|
|
471
|
+
mode: "autopilot",
|
|
472
|
+
outcome: "finish",
|
|
473
|
+
lifecycle_outcome: "finished",
|
|
474
|
+
current_phase: "complete",
|
|
475
|
+
completed_at: "2026-05-20T11:00:00.000Z",
|
|
476
|
+
updated_at: "2026-05-20T11:00:00.000Z",
|
|
477
|
+
});
|
|
478
|
+
const oversizedStop = JSON.stringify({
|
|
479
|
+
hook_event_name: "Stop",
|
|
480
|
+
cwd,
|
|
481
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
482
|
+
});
|
|
483
|
+
assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
|
|
484
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
await rm(cwd, { recursive: true, force: true });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
it("fails closed for oversized non-Stop stdin before parsing", async () => {
|
|
491
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-nonstop-oversized-"));
|
|
492
|
+
try {
|
|
493
|
+
const oversizedPrompt = JSON.stringify({
|
|
494
|
+
hook_event_name: "UserPromptSubmit",
|
|
495
|
+
cwd,
|
|
496
|
+
session_id: "sess-cli-prompt-oversized",
|
|
497
|
+
prompt: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
498
|
+
});
|
|
499
|
+
const output = parseSingleJsonStdout(runNativeHookCli(oversizedPrompt, { cwd }));
|
|
500
|
+
assert.equal(output.continue, false);
|
|
501
|
+
assert.equal(output.stopReason, "native_hook_stdin_oversized");
|
|
502
|
+
assert.match(String(output.systemMessage ?? ""), /rejected oversized stdin JSON before parsing/);
|
|
304
503
|
}
|
|
305
504
|
finally {
|
|
306
505
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -915,6 +1114,50 @@ describe("codex native hook dispatch", () => {
|
|
|
915
1114
|
await rm(cwd, { recursive: true, force: true });
|
|
916
1115
|
}
|
|
917
1116
|
});
|
|
1117
|
+
it("keeps a self-parented native role thread as subagent evidence", async () => {
|
|
1118
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
|
|
1119
|
+
try {
|
|
1120
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1121
|
+
const canonicalSessionId = "omx-autopilot-session";
|
|
1122
|
+
const nativeRoleThreadId = "codex-architect-thread";
|
|
1123
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1124
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1125
|
+
nativeSessionId: nativeRoleThreadId,
|
|
1126
|
+
});
|
|
1127
|
+
const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
|
|
1128
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
1129
|
+
type: "session_meta",
|
|
1130
|
+
payload: {
|
|
1131
|
+
id: nativeRoleThreadId,
|
|
1132
|
+
source: {
|
|
1133
|
+
subagent: {
|
|
1134
|
+
thread_spawn: {
|
|
1135
|
+
parent_thread_id: nativeRoleThreadId,
|
|
1136
|
+
depth: 1,
|
|
1137
|
+
agent_nickname: "Architect",
|
|
1138
|
+
agent_role: "architect",
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
},
|
|
1142
|
+
agent_nickname: "Architect",
|
|
1143
|
+
agent_role: "architect",
|
|
1144
|
+
},
|
|
1145
|
+
})}\n`);
|
|
1146
|
+
await dispatchCodexNativeHook({
|
|
1147
|
+
hook_event_name: "SessionStart",
|
|
1148
|
+
cwd,
|
|
1149
|
+
session_id: nativeRoleThreadId,
|
|
1150
|
+
transcript_path: transcriptPath,
|
|
1151
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
1152
|
+
const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
|
|
1153
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
|
|
1154
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
|
|
1155
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
|
|
1156
|
+
}
|
|
1157
|
+
finally {
|
|
1158
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
918
1161
|
it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
|
|
919
1162
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
|
|
920
1163
|
try {
|
|
@@ -1018,6 +1261,97 @@ describe("codex native hook dispatch", () => {
|
|
|
1018
1261
|
await rm(cwd, { recursive: true, force: true });
|
|
1019
1262
|
}
|
|
1020
1263
|
});
|
|
1264
|
+
it("prefers the OMX owner session id when a native new session revives HUD", async () => {
|
|
1265
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-owner-session-revive-"));
|
|
1266
|
+
try {
|
|
1267
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1268
|
+
const ownerSessionId = "omx-launch-owner-hud";
|
|
1269
|
+
const oldNativeSessionId = "codex-native-hud-old";
|
|
1270
|
+
const nativeSessionId = "codex-native-hud-new";
|
|
1271
|
+
await mkdir(stateDir, { recursive: true });
|
|
1272
|
+
await writeSessionStart(cwd, ownerSessionId, {
|
|
1273
|
+
nativeSessionId: oldNativeSessionId,
|
|
1274
|
+
pid: process.pid,
|
|
1275
|
+
});
|
|
1276
|
+
await dispatchCodexNativeHook({
|
|
1277
|
+
hook_event_name: "SessionStart",
|
|
1278
|
+
cwd,
|
|
1279
|
+
session_id: nativeSessionId,
|
|
1280
|
+
}, {
|
|
1281
|
+
cwd,
|
|
1282
|
+
sessionOwnerPid: process.pid,
|
|
1283
|
+
});
|
|
1284
|
+
const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
|
|
1285
|
+
assert.equal(sessionState.session_id, nativeSessionId);
|
|
1286
|
+
assert.equal(sessionState.native_session_id, nativeSessionId);
|
|
1287
|
+
assert.equal(sessionState.previous_native_session_id, oldNativeSessionId);
|
|
1288
|
+
assert.equal(sessionState.owner_omx_session_id, ownerSessionId);
|
|
1289
|
+
let reconcileCall = null;
|
|
1290
|
+
const promptResult = await dispatchCodexNativeHook({
|
|
1291
|
+
hook_event_name: "UserPromptSubmit",
|
|
1292
|
+
cwd,
|
|
1293
|
+
session_id: nativeSessionId,
|
|
1294
|
+
thread_id: "thread-hud-owner",
|
|
1295
|
+
turn_id: "turn-hud-owner",
|
|
1296
|
+
prompt: "$ralplan fix native new hud owner handoff",
|
|
1297
|
+
}, {
|
|
1298
|
+
cwd,
|
|
1299
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
1300
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId, sessionIds: deps.sessionIds };
|
|
1301
|
+
return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
|
|
1302
|
+
},
|
|
1303
|
+
});
|
|
1304
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
1305
|
+
assert.deepEqual(reconcileCall, {
|
|
1306
|
+
cwd,
|
|
1307
|
+
sessionId: ownerSessionId,
|
|
1308
|
+
sessionIds: [ownerSessionId, nativeSessionId],
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
finally {
|
|
1312
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
it("falls back to the canonical session id for malformed HUD owner ids", async () => {
|
|
1316
|
+
for (const [index, invalidOwnerSessionId] of ["codex-native-hud-owner", "omx-../../stale"].entries()) {
|
|
1317
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-invalid-owner-revive-"));
|
|
1318
|
+
try {
|
|
1319
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1320
|
+
const canonicalSessionId = "omx-launch-hud-safe";
|
|
1321
|
+
const nativeSessionId = "codex-native-hud-safe";
|
|
1322
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1323
|
+
await writeSessionStart(cwd, canonicalSessionId);
|
|
1324
|
+
const sessionStatePath = join(stateDir, "session.json");
|
|
1325
|
+
const sessionState = JSON.parse(await readFile(sessionStatePath, "utf-8"));
|
|
1326
|
+
sessionState.owner_omx_session_id = invalidOwnerSessionId;
|
|
1327
|
+
await writeJson(sessionStatePath, sessionState);
|
|
1328
|
+
let reconcileCall = null;
|
|
1329
|
+
const promptResult = await dispatchCodexNativeHook({
|
|
1330
|
+
hook_event_name: "UserPromptSubmit",
|
|
1331
|
+
cwd,
|
|
1332
|
+
session_id: nativeSessionId,
|
|
1333
|
+
thread_id: `thread-hud-invalid-owner-${index}`,
|
|
1334
|
+
turn_id: "turn-hud-invalid-owner",
|
|
1335
|
+
prompt: "$ralplan fix malformed hud owner handoff",
|
|
1336
|
+
}, {
|
|
1337
|
+
cwd,
|
|
1338
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
1339
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId, sessionIds: deps.sessionIds };
|
|
1340
|
+
return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
1344
|
+
assert.deepEqual(reconcileCall, {
|
|
1345
|
+
cwd,
|
|
1346
|
+
sessionId: canonicalSessionId,
|
|
1347
|
+
sessionIds: [canonicalSessionId, nativeSessionId],
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
finally {
|
|
1351
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1021
1355
|
it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
|
|
1022
1356
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
|
|
1023
1357
|
try {
|
|
@@ -2208,75 +2542,376 @@ standardMaxRounds = 15
|
|
|
2208
2542
|
await rm(cwd, { recursive: true, force: true });
|
|
2209
2543
|
}
|
|
2210
2544
|
});
|
|
2211
|
-
it("
|
|
2212
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
2213
|
-
try {
|
|
2214
|
-
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2215
|
-
const result = await dispatchCodexNativeHook({
|
|
2216
|
-
hook_event_name: "UserPromptSubmit",
|
|
2217
|
-
cwd,
|
|
2218
|
-
session_id: "sess-plugin-1",
|
|
2219
|
-
thread_id: "thread-plugin-1",
|
|
2220
|
-
turn_id: "turn-plugin-1",
|
|
2221
|
-
prompt: "$oh-my-codex:ralplan implement issue #1307",
|
|
2222
|
-
}, { cwd });
|
|
2223
|
-
assert.equal(result.omxEventName, "keyword-detector");
|
|
2224
|
-
assert.equal(result.skillState?.skill, "ralplan");
|
|
2225
|
-
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2226
|
-
assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
|
|
2227
|
-
assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
|
|
2228
|
-
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
|
|
2229
|
-
}
|
|
2230
|
-
finally {
|
|
2231
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2232
|
-
}
|
|
2233
|
-
});
|
|
2234
|
-
it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
|
|
2235
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
|
|
2545
|
+
it("does not treat a corrupt leader kind=subagent tracker entry as native subagent prompt scope", async () => {
|
|
2546
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-corrupt-leader-subagent-"));
|
|
2236
2547
|
try {
|
|
2237
|
-
|
|
2548
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2549
|
+
const canonicalSessionId = "sess-corrupt-leader";
|
|
2550
|
+
const leaderNativeSessionId = "native-corrupt-leader";
|
|
2551
|
+
const nowIso = new Date().toISOString();
|
|
2552
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2553
|
+
session_id: canonicalSessionId,
|
|
2554
|
+
native_session_id: leaderNativeSessionId,
|
|
2555
|
+
});
|
|
2556
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2557
|
+
schemaVersion: 1,
|
|
2558
|
+
sessions: {
|
|
2559
|
+
[canonicalSessionId]: {
|
|
2560
|
+
session_id: canonicalSessionId,
|
|
2561
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2562
|
+
updated_at: nowIso,
|
|
2563
|
+
threads: {
|
|
2564
|
+
[leaderNativeSessionId]: {
|
|
2565
|
+
thread_id: leaderNativeSessionId,
|
|
2566
|
+
kind: "subagent",
|
|
2567
|
+
first_seen_at: nowIso,
|
|
2568
|
+
last_seen_at: nowIso,
|
|
2569
|
+
turn_count: 2,
|
|
2570
|
+
},
|
|
2571
|
+
},
|
|
2572
|
+
},
|
|
2573
|
+
},
|
|
2574
|
+
});
|
|
2238
2575
|
const result = await dispatchCodexNativeHook({
|
|
2239
2576
|
hook_event_name: "UserPromptSubmit",
|
|
2240
2577
|
cwd,
|
|
2241
|
-
session_id:
|
|
2242
|
-
thread_id:
|
|
2243
|
-
turn_id: "turn-
|
|
2244
|
-
prompt: "$autopilot
|
|
2578
|
+
session_id: leaderNativeSessionId,
|
|
2579
|
+
thread_id: leaderNativeSessionId,
|
|
2580
|
+
turn_id: "turn-corrupt-leader",
|
|
2581
|
+
prompt: "$autopilot continue this review blocker fix",
|
|
2245
2582
|
}, { cwd });
|
|
2246
2583
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
2247
2584
|
assert.equal(result.skillState?.skill, "autopilot");
|
|
2248
|
-
|
|
2249
|
-
assert.match(message, /Autopilot protocol:/);
|
|
2250
|
-
assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
|
|
2251
|
-
assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
|
|
2252
|
-
assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
|
|
2585
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2253
2586
|
}
|
|
2254
2587
|
finally {
|
|
2255
2588
|
await rm(cwd, { recursive: true, force: true });
|
|
2256
2589
|
}
|
|
2257
2590
|
});
|
|
2258
|
-
it("
|
|
2259
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
2591
|
+
it("lets the current canonical leader boundary beat stale global subagent tracking with a distinct prompt thread id", async () => {
|
|
2592
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-leader-stale-global-"));
|
|
2260
2593
|
try {
|
|
2261
|
-
|
|
2262
|
-
const
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2594
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2595
|
+
const canonicalSessionId = "sess-current-leader";
|
|
2596
|
+
const leaderNativeSessionId = "native-current-leader";
|
|
2597
|
+
const staleSessionId = "sess-stale-subagent";
|
|
2598
|
+
const staleLeaderNativeSessionId = "native-stale-leader";
|
|
2599
|
+
const nowIso = new Date().toISOString();
|
|
2600
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2601
|
+
session_id: canonicalSessionId,
|
|
2602
|
+
native_session_id: leaderNativeSessionId,
|
|
2603
|
+
});
|
|
2604
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2605
|
+
schemaVersion: 1,
|
|
2606
|
+
sessions: {
|
|
2607
|
+
[canonicalSessionId]: {
|
|
2608
|
+
session_id: canonicalSessionId,
|
|
2609
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2610
|
+
updated_at: nowIso,
|
|
2611
|
+
threads: {
|
|
2612
|
+
[leaderNativeSessionId]: {
|
|
2613
|
+
thread_id: leaderNativeSessionId,
|
|
2614
|
+
kind: "leader",
|
|
2615
|
+
first_seen_at: nowIso,
|
|
2616
|
+
last_seen_at: nowIso,
|
|
2617
|
+
turn_count: 1,
|
|
2618
|
+
},
|
|
2619
|
+
},
|
|
2620
|
+
},
|
|
2621
|
+
[staleSessionId]: {
|
|
2622
|
+
session_id: staleSessionId,
|
|
2623
|
+
leader_thread_id: staleLeaderNativeSessionId,
|
|
2624
|
+
updated_at: nowIso,
|
|
2625
|
+
threads: {
|
|
2626
|
+
[staleLeaderNativeSessionId]: {
|
|
2627
|
+
thread_id: staleLeaderNativeSessionId,
|
|
2628
|
+
kind: "leader",
|
|
2629
|
+
first_seen_at: nowIso,
|
|
2630
|
+
last_seen_at: nowIso,
|
|
2631
|
+
turn_count: 1,
|
|
2632
|
+
},
|
|
2633
|
+
[leaderNativeSessionId]: {
|
|
2634
|
+
thread_id: leaderNativeSessionId,
|
|
2635
|
+
kind: "subagent",
|
|
2636
|
+
first_seen_at: nowIso,
|
|
2637
|
+
last_seen_at: nowIso,
|
|
2638
|
+
turn_count: 1,
|
|
2639
|
+
mode: "architect",
|
|
2640
|
+
},
|
|
2641
|
+
},
|
|
2642
|
+
},
|
|
2643
|
+
},
|
|
2644
|
+
});
|
|
2645
|
+
const result = await dispatchCodexNativeHook({
|
|
2646
|
+
hook_event_name: "UserPromptSubmit",
|
|
2647
|
+
cwd,
|
|
2648
|
+
session_id: leaderNativeSessionId,
|
|
2649
|
+
thread_id: "thread-current-turn-not-native-session",
|
|
2650
|
+
turn_id: "turn-current-leader",
|
|
2651
|
+
prompt: "$autopilot continue",
|
|
2652
|
+
}, { cwd });
|
|
2653
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2654
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2655
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2656
|
+
assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
|
|
2657
|
+
}
|
|
2658
|
+
finally {
|
|
2659
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
it("lets the current session native leader beat stale global subagent tracking without a canonical summary and with a distinct prompt thread id", async () => {
|
|
2663
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-stale-global-"));
|
|
2664
|
+
try {
|
|
2665
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2666
|
+
const canonicalSessionId = "sess-current-native-leader";
|
|
2667
|
+
const leaderNativeSessionId = "native-current-leader-no-summary";
|
|
2668
|
+
const staleSessionId = "sess-stale-native-subagent";
|
|
2669
|
+
const staleLeaderNativeSessionId = "native-stale-parent";
|
|
2670
|
+
const nowIso = new Date().toISOString();
|
|
2671
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2672
|
+
session_id: canonicalSessionId,
|
|
2673
|
+
native_session_id: leaderNativeSessionId,
|
|
2674
|
+
});
|
|
2675
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2676
|
+
schemaVersion: 1,
|
|
2677
|
+
sessions: {
|
|
2678
|
+
[staleSessionId]: {
|
|
2679
|
+
session_id: staleSessionId,
|
|
2680
|
+
leader_thread_id: staleLeaderNativeSessionId,
|
|
2681
|
+
updated_at: nowIso,
|
|
2682
|
+
threads: {
|
|
2683
|
+
[staleLeaderNativeSessionId]: {
|
|
2684
|
+
thread_id: staleLeaderNativeSessionId,
|
|
2685
|
+
kind: "leader",
|
|
2686
|
+
first_seen_at: nowIso,
|
|
2687
|
+
last_seen_at: nowIso,
|
|
2688
|
+
turn_count: 1,
|
|
2689
|
+
},
|
|
2690
|
+
[leaderNativeSessionId]: {
|
|
2691
|
+
thread_id: leaderNativeSessionId,
|
|
2692
|
+
kind: "subagent",
|
|
2693
|
+
first_seen_at: nowIso,
|
|
2694
|
+
last_seen_at: nowIso,
|
|
2695
|
+
turn_count: 1,
|
|
2696
|
+
mode: "critic",
|
|
2697
|
+
},
|
|
2698
|
+
},
|
|
2699
|
+
},
|
|
2700
|
+
},
|
|
2701
|
+
});
|
|
2702
|
+
const result = await dispatchCodexNativeHook({
|
|
2703
|
+
hook_event_name: "UserPromptSubmit",
|
|
2704
|
+
cwd,
|
|
2705
|
+
session_id: leaderNativeSessionId,
|
|
2706
|
+
thread_id: "thread-current-turn-not-native-session",
|
|
2707
|
+
turn_id: "turn-current-native-leader",
|
|
2708
|
+
prompt: "$autopilot continue",
|
|
2709
|
+
}, { cwd });
|
|
2710
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2711
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2712
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2713
|
+
assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
|
|
2714
|
+
}
|
|
2715
|
+
finally {
|
|
2716
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
it("lets the current session native leader beat a malformed canonical subagent entry", async () => {
|
|
2720
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-malformed-canonical-"));
|
|
2721
|
+
try {
|
|
2722
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2723
|
+
const canonicalSessionId = "sess-current-native-leader-malformed";
|
|
2724
|
+
const leaderNativeSessionId = "native-current-leader-malformed";
|
|
2725
|
+
const nowIso = new Date().toISOString();
|
|
2726
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2727
|
+
session_id: canonicalSessionId,
|
|
2728
|
+
native_session_id: leaderNativeSessionId,
|
|
2729
|
+
});
|
|
2730
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2731
|
+
schemaVersion: 1,
|
|
2732
|
+
sessions: {
|
|
2733
|
+
[canonicalSessionId]: {
|
|
2734
|
+
session_id: canonicalSessionId,
|
|
2735
|
+
updated_at: nowIso,
|
|
2736
|
+
threads: {
|
|
2737
|
+
[leaderNativeSessionId]: {
|
|
2738
|
+
thread_id: leaderNativeSessionId,
|
|
2739
|
+
kind: "subagent",
|
|
2740
|
+
first_seen_at: nowIso,
|
|
2741
|
+
last_seen_at: nowIso,
|
|
2742
|
+
turn_count: 1,
|
|
2743
|
+
mode: "architect",
|
|
2744
|
+
},
|
|
2745
|
+
},
|
|
2746
|
+
},
|
|
2747
|
+
},
|
|
2748
|
+
});
|
|
2749
|
+
const result = await dispatchCodexNativeHook({
|
|
2750
|
+
hook_event_name: "UserPromptSubmit",
|
|
2751
|
+
cwd,
|
|
2752
|
+
session_id: leaderNativeSessionId,
|
|
2753
|
+
thread_id: leaderNativeSessionId,
|
|
2754
|
+
turn_id: "turn-current-native-leader-malformed",
|
|
2755
|
+
prompt: "$autopilot continue",
|
|
2756
|
+
}, { cwd });
|
|
2757
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2758
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2759
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2760
|
+
}
|
|
2761
|
+
finally {
|
|
2762
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
it("still treats mixed child and leader payload identities as native subagent scope", async () => {
|
|
2766
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-mixed-child-leader-identity-"));
|
|
2767
|
+
try {
|
|
2768
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2769
|
+
const canonicalSessionId = "sess-mixed-child-leader";
|
|
2770
|
+
const leaderNativeSessionId = "native-mixed-leader";
|
|
2771
|
+
const childNativeSessionId = "native-mixed-child";
|
|
2772
|
+
const nowIso = new Date().toISOString();
|
|
2773
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2774
|
+
session_id: canonicalSessionId,
|
|
2775
|
+
native_session_id: leaderNativeSessionId,
|
|
2776
|
+
});
|
|
2777
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2778
|
+
schemaVersion: 1,
|
|
2779
|
+
sessions: {
|
|
2780
|
+
[canonicalSessionId]: {
|
|
2781
|
+
session_id: canonicalSessionId,
|
|
2782
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2783
|
+
updated_at: nowIso,
|
|
2784
|
+
threads: {
|
|
2785
|
+
[leaderNativeSessionId]: {
|
|
2786
|
+
thread_id: leaderNativeSessionId,
|
|
2787
|
+
kind: "leader",
|
|
2788
|
+
first_seen_at: nowIso,
|
|
2789
|
+
last_seen_at: nowIso,
|
|
2790
|
+
turn_count: 1,
|
|
2791
|
+
},
|
|
2792
|
+
[childNativeSessionId]: {
|
|
2793
|
+
thread_id: childNativeSessionId,
|
|
2794
|
+
kind: "subagent",
|
|
2795
|
+
first_seen_at: nowIso,
|
|
2796
|
+
last_seen_at: nowIso,
|
|
2797
|
+
turn_count: 1,
|
|
2798
|
+
mode: "critic",
|
|
2799
|
+
},
|
|
2800
|
+
},
|
|
2801
|
+
},
|
|
2802
|
+
},
|
|
2803
|
+
});
|
|
2804
|
+
const result = await dispatchCodexNativeHook({
|
|
2805
|
+
hook_event_name: "UserPromptSubmit",
|
|
2806
|
+
cwd,
|
|
2807
|
+
session_id: childNativeSessionId,
|
|
2808
|
+
thread_id: leaderNativeSessionId,
|
|
2809
|
+
turn_id: "turn-mixed-child-leader",
|
|
2810
|
+
prompt: "$ralplan review this as delegated text",
|
|
2811
|
+
}, { cwd });
|
|
2812
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2813
|
+
assert.equal(result.skillState, null);
|
|
2814
|
+
assert.equal(result.outputJson, null);
|
|
2815
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
|
|
2816
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
|
|
2817
|
+
const reversedResult = await dispatchCodexNativeHook({
|
|
2818
|
+
hook_event_name: "UserPromptSubmit",
|
|
2819
|
+
cwd,
|
|
2820
|
+
session_id: leaderNativeSessionId,
|
|
2821
|
+
thread_id: childNativeSessionId,
|
|
2822
|
+
turn_id: "turn-mixed-leader-child",
|
|
2823
|
+
prompt: "$autopilot review this as delegated text",
|
|
2824
|
+
}, { cwd });
|
|
2825
|
+
assert.equal(reversedResult.omxEventName, "keyword-detector");
|
|
2826
|
+
assert.equal(reversedResult.skillState, null);
|
|
2827
|
+
assert.equal(reversedResult.outputJson, null);
|
|
2828
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), false);
|
|
2829
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "autopilot-state.json")), false);
|
|
2830
|
+
}
|
|
2831
|
+
finally {
|
|
2832
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2835
|
+
it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
|
|
2836
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
|
|
2837
|
+
try {
|
|
2838
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2839
|
+
const result = await dispatchCodexNativeHook({
|
|
2840
|
+
hook_event_name: "UserPromptSubmit",
|
|
2841
|
+
cwd,
|
|
2842
|
+
session_id: "sess-plugin-1",
|
|
2843
|
+
thread_id: "thread-plugin-1",
|
|
2844
|
+
turn_id: "turn-plugin-1",
|
|
2845
|
+
prompt: "$oh-my-codex:ralplan implement issue #1307",
|
|
2846
|
+
}, { cwd });
|
|
2847
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2848
|
+
assert.equal(result.skillState?.skill, "ralplan");
|
|
2849
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2850
|
+
assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
|
|
2851
|
+
assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
|
|
2852
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
|
|
2853
|
+
}
|
|
2854
|
+
finally {
|
|
2855
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
|
|
2859
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
|
|
2860
|
+
try {
|
|
2861
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2862
|
+
const result = await dispatchCodexNativeHook({
|
|
2863
|
+
hook_event_name: "UserPromptSubmit",
|
|
2864
|
+
cwd,
|
|
2865
|
+
session_id: "sess-autopilot-ralplan-gate",
|
|
2866
|
+
thread_id: "thread-autopilot-ralplan-gate",
|
|
2867
|
+
turn_id: "turn-autopilot-ralplan-gate",
|
|
2868
|
+
prompt: "$autopilot implement issue #2430",
|
|
2869
|
+
}, { cwd });
|
|
2870
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2871
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2872
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2873
|
+
assert.match(message, /Autopilot protocol:/);
|
|
2874
|
+
assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
|
|
2875
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
2876
|
+
assert.match(message, /re-score ambiguity against the active threshold/);
|
|
2877
|
+
assert.match(message, /max_rounds as a cap/);
|
|
2878
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
2879
|
+
assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
|
|
2880
|
+
assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
|
|
2881
|
+
const autopilotState = JSON.parse(await readFile(join(cwd, ".omx", "state", "sessions", "sess-autopilot-ralplan-gate", "autopilot-state.json"), "utf-8"));
|
|
2882
|
+
const snapshotPath = autopilotState.state?.handoff_artifacts?.context_snapshot_path ?? "";
|
|
2883
|
+
assert.match(snapshotPath, /^\.omx\/context\/implement-issue-2430-\d{8}T\d{6}Z\.md$/);
|
|
2884
|
+
const snapshot = await readFile(join(cwd, snapshotPath), "utf-8");
|
|
2885
|
+
assert.match(snapshot, /activation prompt \/ task seed: \$autopilot implement issue #2430/);
|
|
2886
|
+
assert.match(snapshot, /scope note: this seed captures the Autopilot activation prompt/);
|
|
2887
|
+
assert.match(snapshot, /constraints: follow deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
|
|
2888
|
+
}
|
|
2889
|
+
finally {
|
|
2890
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
|
|
2894
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
|
|
2895
|
+
try {
|
|
2896
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2897
|
+
const result = await dispatchCodexNativeHook({
|
|
2898
|
+
hook_event_name: "UserPromptSubmit",
|
|
2899
|
+
cwd,
|
|
2900
|
+
session_id: "sess-ultragoal-1",
|
|
2901
|
+
thread_id: "thread-ultragoal-1",
|
|
2902
|
+
turn_id: "turn-ultragoal-1",
|
|
2903
|
+
prompt: "$ultragoal split this launch into durable goals",
|
|
2904
|
+
}, { cwd });
|
|
2905
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2906
|
+
assert.equal(result.skillState?.skill, "ultragoal");
|
|
2907
|
+
assert.equal(result.skillState?.initialized_mode, "ultragoal");
|
|
2908
|
+
assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-ultragoal-1/ultragoal-state.json");
|
|
2909
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2910
|
+
assert.match(message, /"\$ultragoal" -> ultragoal/);
|
|
2911
|
+
assert.match(message, /Ultragoal protocol:/);
|
|
2912
|
+
assert.match(message, /get_goal/);
|
|
2913
|
+
assert.match(message, /create_goal/);
|
|
2914
|
+
assert.match(message, /update_goal/);
|
|
2280
2915
|
assert.match(message, /does not call `\/goal clear`/);
|
|
2281
2916
|
assert.match(message, /multiple sequential ultragoal runs/);
|
|
2282
2917
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), true);
|
|
@@ -2705,6 +3340,11 @@ ${JSON.stringify({
|
|
|
2705
3340
|
assert.equal(result.skillState?.skill, "autopilot");
|
|
2706
3341
|
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2707
3342
|
assert.match(message, /"keep going" -> ralph/);
|
|
3343
|
+
assert.match(message, /Autopilot protocol:/);
|
|
3344
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
3345
|
+
assert.match(message, /re-score ambiguity against the active threshold/);
|
|
3346
|
+
assert.match(message, /max_rounds as a cap/);
|
|
3347
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
2708
3348
|
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
2709
3349
|
assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
|
|
2710
3350
|
assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
|
|
@@ -2714,6 +3354,107 @@ ${JSON.stringify({
|
|
|
2714
3354
|
await rm(cwd, { recursive: true, force: true });
|
|
2715
3355
|
}
|
|
2716
3356
|
});
|
|
3357
|
+
it("keeps omx question answers on the active autopilot skill so the interview chain guidance is injected", async () => {
|
|
3358
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-question-answer-continuation-"));
|
|
3359
|
+
try {
|
|
3360
|
+
const sessionId = "sess-autopilot-question-answer";
|
|
3361
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
3362
|
+
await mkdir(sessionDir, { recursive: true });
|
|
3363
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
3364
|
+
version: 1,
|
|
3365
|
+
active: true,
|
|
3366
|
+
skill: "autopilot",
|
|
3367
|
+
keyword: "$autopilot",
|
|
3368
|
+
phase: "deep-interview",
|
|
3369
|
+
initialized_mode: "autopilot",
|
|
3370
|
+
initialized_state_path: `.omx/state/sessions/${sessionId}/autopilot-state.json`,
|
|
3371
|
+
session_id: sessionId,
|
|
3372
|
+
active_skills: [
|
|
3373
|
+
{ skill: "autopilot", phase: "deep-interview", active: true, session_id: sessionId },
|
|
3374
|
+
],
|
|
3375
|
+
});
|
|
3376
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
3377
|
+
active: true,
|
|
3378
|
+
mode: "autopilot",
|
|
3379
|
+
current_phase: "deep-interview",
|
|
3380
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
3381
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
3382
|
+
session_id: sessionId,
|
|
3383
|
+
});
|
|
3384
|
+
const result = await dispatchCodexNativeHook({
|
|
3385
|
+
hook_event_name: "UserPromptSubmit",
|
|
3386
|
+
cwd,
|
|
3387
|
+
session_id: sessionId,
|
|
3388
|
+
thread_id: "thread-autopilot-question-answer",
|
|
3389
|
+
turn_id: "turn-autopilot-question-answer",
|
|
3390
|
+
prompt: "[omx question answered] semantic_marker_expansion $ralplan",
|
|
3391
|
+
}, { cwd });
|
|
3392
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3393
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
3394
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
3395
|
+
assert.match(message, /continued active workflow skill "autopilot"/);
|
|
3396
|
+
assert.match(message, /Autopilot protocol:/);
|
|
3397
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
3398
|
+
assert.match(message, /This turn is a marked omx question answer/);
|
|
3399
|
+
assert.match(message, /then re-score/);
|
|
3400
|
+
assert.match(message, /write interview_complete evidence and hand off/);
|
|
3401
|
+
assert.match(message, /readiness gate remains unresolved and the answer would materially change execution/);
|
|
3402
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
3403
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
3404
|
+
assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
|
|
3405
|
+
}
|
|
3406
|
+
finally {
|
|
3407
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
it("keeps deep-interview bridge guidance on marked question answers with workflow-like tokens", async () => {
|
|
3411
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-question-answer-continuation-"));
|
|
3412
|
+
try {
|
|
3413
|
+
const sessionId = "sess-deep-interview-question-answer";
|
|
3414
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
3415
|
+
await mkdir(sessionDir, { recursive: true });
|
|
3416
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
3417
|
+
version: 1,
|
|
3418
|
+
active: true,
|
|
3419
|
+
skill: "deep-interview",
|
|
3420
|
+
keyword: "$deep-interview",
|
|
3421
|
+
phase: "planning",
|
|
3422
|
+
initialized_mode: "deep-interview",
|
|
3423
|
+
initialized_state_path: `.omx/state/sessions/${sessionId}/deep-interview-state.json`,
|
|
3424
|
+
session_id: sessionId,
|
|
3425
|
+
active_skills: [
|
|
3426
|
+
{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId },
|
|
3427
|
+
],
|
|
3428
|
+
});
|
|
3429
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
3430
|
+
active: true,
|
|
3431
|
+
mode: "deep-interview",
|
|
3432
|
+
current_phase: "intent-first",
|
|
3433
|
+
started_at: "2026-04-21T10:00:00.000Z",
|
|
3434
|
+
updated_at: "2026-04-21T10:00:00.000Z",
|
|
3435
|
+
});
|
|
3436
|
+
const result = await dispatchCodexNativeHook({
|
|
3437
|
+
hook_event_name: "UserPromptSubmit",
|
|
3438
|
+
cwd,
|
|
3439
|
+
session_id: sessionId,
|
|
3440
|
+
thread_id: "thread-deep-interview-question-answer",
|
|
3441
|
+
turn_id: "turn-deep-interview-question-answer",
|
|
3442
|
+
prompt: "[omx question answered] answer text $ralplan",
|
|
3443
|
+
}, { cwd });
|
|
3444
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3445
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
3446
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
3447
|
+
assert.match(message, /continued active workflow skill "deep-interview"/);
|
|
3448
|
+
assert.match(message, /workflow-like tokens inside the marked omx question answer are treated as answer text/);
|
|
3449
|
+
assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
|
|
3450
|
+
assert.match(message, /native structured question tool when available/);
|
|
3451
|
+
assert.doesNotMatch(message, /detected workflow keyword "\$ralplan" -> ralplan/);
|
|
3452
|
+
assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
|
|
3453
|
+
}
|
|
3454
|
+
finally {
|
|
3455
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
2717
3458
|
it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
|
|
2718
3459
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
|
|
2719
3460
|
try {
|
|
@@ -3140,6 +3881,10 @@ export async function onHookEvent(event) {
|
|
|
3140
3881
|
active: true,
|
|
3141
3882
|
mode: "deep-interview",
|
|
3142
3883
|
current_phase: "intent-first",
|
|
3884
|
+
deep_interview_gate: {
|
|
3885
|
+
status: "complete",
|
|
3886
|
+
rationale: "Requirements are clarified and ready for ralplan consensus.",
|
|
3887
|
+
},
|
|
3143
3888
|
});
|
|
3144
3889
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
3145
3890
|
active: true,
|
|
@@ -3459,7 +4204,54 @@ esac
|
|
|
3459
4204
|
await rm(cwd, { recursive: true, force: true });
|
|
3460
4205
|
}
|
|
3461
4206
|
});
|
|
3462
|
-
it("
|
|
4207
|
+
it("skips prompt-submit HUD reconciliation during doctor smoke validation", async () => {
|
|
4208
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-doctor-smoke-hud-"));
|
|
4209
|
+
const originalTmux = process.env.TMUX;
|
|
4210
|
+
const originalTmuxPane = process.env.TMUX_PANE;
|
|
4211
|
+
const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
4212
|
+
const originalDoctorSmoke = process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE;
|
|
4213
|
+
try {
|
|
4214
|
+
process.env.TMUX = "1";
|
|
4215
|
+
process.env.TMUX_PANE = "%1";
|
|
4216
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
|
|
4217
|
+
process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE = "1";
|
|
4218
|
+
let reconcileCalled = false;
|
|
4219
|
+
const result = await dispatchCodexNativeHook({
|
|
4220
|
+
hook_event_name: "UserPromptSubmit",
|
|
4221
|
+
cwd,
|
|
4222
|
+
session_id: "omx-doctor-plugin-hook-smoke",
|
|
4223
|
+
prompt: "$ralplan doctor plugin hook smoke test",
|
|
4224
|
+
}, {
|
|
4225
|
+
cwd,
|
|
4226
|
+
reconcileHudForPromptSubmitFn: async () => {
|
|
4227
|
+
reconcileCalled = true;
|
|
4228
|
+
return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
|
|
4229
|
+
},
|
|
4230
|
+
});
|
|
4231
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
4232
|
+
assert.equal(reconcileCalled, false);
|
|
4233
|
+
}
|
|
4234
|
+
finally {
|
|
4235
|
+
if (originalTmux === undefined)
|
|
4236
|
+
delete process.env.TMUX;
|
|
4237
|
+
else
|
|
4238
|
+
process.env.TMUX = originalTmux;
|
|
4239
|
+
if (originalTmuxPane === undefined)
|
|
4240
|
+
delete process.env.TMUX_PANE;
|
|
4241
|
+
else
|
|
4242
|
+
process.env.TMUX_PANE = originalTmuxPane;
|
|
4243
|
+
if (originalHudOwner === undefined)
|
|
4244
|
+
delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
4245
|
+
else
|
|
4246
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
|
|
4247
|
+
if (originalDoctorSmoke === undefined)
|
|
4248
|
+
delete process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE;
|
|
4249
|
+
else
|
|
4250
|
+
process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE = originalDoctorSmoke;
|
|
4251
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4252
|
+
}
|
|
4253
|
+
});
|
|
4254
|
+
it("recreates a leader-only HUD pane when UserPromptSubmit revives with the canonical session id", async () => {
|
|
3463
4255
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-"));
|
|
3464
4256
|
const originalTmux = process.env.TMUX;
|
|
3465
4257
|
const originalTmuxPane = process.env.TMUX_PANE;
|
|
@@ -3506,8 +4298,8 @@ esac
|
|
|
3506
4298
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
3507
4299
|
const tmuxCalls = await readFile(tmuxLog, "utf-8");
|
|
3508
4300
|
assert.match(tmuxCalls, /list-panes -t %1 -F/);
|
|
3509
|
-
assert.match(tmuxCalls,
|
|
3510
|
-
assert.
|
|
4301
|
+
assert.match(tmuxCalls, /split-window/);
|
|
4302
|
+
assert.match(tmuxCalls, new RegExp(`resize-pane -t %9 -y ${HUD_TMUX_HEIGHT_LINES}`));
|
|
3511
4303
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", canonicalSessionId, "ralplan-state.json")), true);
|
|
3512
4304
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", nativeSessionId, "ralplan-state.json")), false);
|
|
3513
4305
|
}
|
|
@@ -5758,6 +6550,42 @@ exit 0
|
|
|
5758
6550
|
await rm(cwd, { recursive: true, force: true });
|
|
5759
6551
|
}
|
|
5760
6552
|
});
|
|
6553
|
+
it("does not block ordinary non-zero grep output in PostToolUse", async () => {
|
|
6554
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-grep-nonzero-"));
|
|
6555
|
+
try {
|
|
6556
|
+
const result = await dispatchCodexNativeHook({
|
|
6557
|
+
hook_event_name: "PostToolUse",
|
|
6558
|
+
cwd,
|
|
6559
|
+
tool_name: "Bash",
|
|
6560
|
+
tool_use_id: "tool-grep-nonzero",
|
|
6561
|
+
tool_input: { command: "grep -R missing-pattern src | head -20" },
|
|
6562
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"src/example.ts:TODO\",\"stderr\":\"\"}",
|
|
6563
|
+
}, { cwd });
|
|
6564
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6565
|
+
assert.equal(result.outputJson, null);
|
|
6566
|
+
}
|
|
6567
|
+
finally {
|
|
6568
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6569
|
+
}
|
|
6570
|
+
});
|
|
6571
|
+
it("does not block ordinary non-zero diagnostic output in PostToolUse", async () => {
|
|
6572
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-diagnostic-nonzero-"));
|
|
6573
|
+
try {
|
|
6574
|
+
const result = await dispatchCodexNativeHook({
|
|
6575
|
+
hook_event_name: "PostToolUse",
|
|
6576
|
+
cwd,
|
|
6577
|
+
tool_name: "Bash",
|
|
6578
|
+
tool_use_id: "tool-diagnostic-nonzero",
|
|
6579
|
+
tool_input: { command: "find src -name nope -print" },
|
|
6580
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"searched 10 files\",\"stderr\":\"\"}",
|
|
6581
|
+
}, { cwd });
|
|
6582
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6583
|
+
assert.equal(result.outputJson, null);
|
|
6584
|
+
}
|
|
6585
|
+
finally {
|
|
6586
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6587
|
+
}
|
|
6588
|
+
});
|
|
5761
6589
|
it("treats stderr-only informative non-zero output as reviewable instead of a generic failure", async () => {
|
|
5762
6590
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-informative-stderr-"));
|
|
5763
6591
|
try {
|
|
@@ -5808,6 +6636,72 @@ exit 0
|
|
|
5808
6636
|
await rm(cwd, { recursive: true, force: true });
|
|
5809
6637
|
}
|
|
5810
6638
|
});
|
|
6639
|
+
it("treats wrapped gh pr checks output as reviewable", async () => {
|
|
6640
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-wrapped-"));
|
|
6641
|
+
try {
|
|
6642
|
+
for (const command of [
|
|
6643
|
+
"GH_PAGER=cat gh pr checks",
|
|
6644
|
+
"env GH_TOKEN=ghp_testtoken gh pr checks",
|
|
6645
|
+
"/usr/bin/env gh pr checks",
|
|
6646
|
+
"env -- gh pr checks",
|
|
6647
|
+
"env -C repo gh pr checks",
|
|
6648
|
+
"/usr/bin/gh pr checks",
|
|
6649
|
+
"gh --repo owner/repo pr checks",
|
|
6650
|
+
"echo a; gh pr checks",
|
|
6651
|
+
"cd repo && gh pr checks",
|
|
6652
|
+
]) {
|
|
6653
|
+
const result = await dispatchCodexNativeHook({
|
|
6654
|
+
hook_event_name: "PostToolUse",
|
|
6655
|
+
cwd,
|
|
6656
|
+
tool_name: "Bash",
|
|
6657
|
+
tool_use_id: `tool-useful-${command}`,
|
|
6658
|
+
tool_input: { command },
|
|
6659
|
+
tool_response: "{\"exit_code\":8,\"stdout\":\"build pending\",\"stderr\":\"\"}",
|
|
6660
|
+
}, { cwd });
|
|
6661
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6662
|
+
assert.equal(result.outputJson?.decision, "block", command);
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
finally {
|
|
6666
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6667
|
+
}
|
|
6668
|
+
});
|
|
6669
|
+
it("does not treat heredoc gh pr checks text as a reviewable command", async () => {
|
|
6670
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-heredoc-"));
|
|
6671
|
+
try {
|
|
6672
|
+
const result = await dispatchCodexNativeHook({
|
|
6673
|
+
hook_event_name: "PostToolUse",
|
|
6674
|
+
cwd,
|
|
6675
|
+
tool_name: "Bash",
|
|
6676
|
+
tool_use_id: "tool-heredoc-gh-checks",
|
|
6677
|
+
tool_input: { command: "cat <<'EOF'\ngh pr checks\nEOF\nfalse" },
|
|
6678
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
|
|
6679
|
+
}, { cwd });
|
|
6680
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6681
|
+
assert.equal(result.outputJson, null);
|
|
6682
|
+
}
|
|
6683
|
+
finally {
|
|
6684
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6685
|
+
}
|
|
6686
|
+
});
|
|
6687
|
+
it("does not treat echoed gh pr checks text as a reviewable command", async () => {
|
|
6688
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-echo-"));
|
|
6689
|
+
try {
|
|
6690
|
+
const result = await dispatchCodexNativeHook({
|
|
6691
|
+
hook_event_name: "PostToolUse",
|
|
6692
|
+
cwd,
|
|
6693
|
+
tool_name: "Bash",
|
|
6694
|
+
tool_use_id: "tool-echo-gh-checks",
|
|
6695
|
+
tool_input: { command: "echo gh pr checks" },
|
|
6696
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
|
|
6697
|
+
}, { cwd });
|
|
6698
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6699
|
+
assert.equal(result.outputJson, null);
|
|
6700
|
+
}
|
|
6701
|
+
finally {
|
|
6702
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6703
|
+
}
|
|
6704
|
+
});
|
|
5811
6705
|
it("returns MCP transport-death guidance and preserves failed team state", async () => {
|
|
5812
6706
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
|
|
5813
6707
|
try {
|
|
@@ -6649,63 +7543,49 @@ exit 0
|
|
|
6649
7543
|
await rm(cwd, { recursive: true, force: true });
|
|
6650
7544
|
}
|
|
6651
7545
|
});
|
|
6652
|
-
it("
|
|
6653
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-
|
|
6654
|
-
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
6655
|
-
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
7546
|
+
it("dedupes allowed worker Stop leader nudges across workers in the same team window", async () => {
|
|
7547
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-team-dedupe-"));
|
|
6656
7548
|
const prevPath = process.env.PATH;
|
|
6657
7549
|
try {
|
|
6658
|
-
|
|
7550
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7551
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7552
|
+
const teamName = "worker-stop-team-dedupe";
|
|
7553
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
6659
7554
|
const fakeBinDir = join(cwd, "fake-bin");
|
|
7555
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
6660
7556
|
await mkdir(fakeBinDir, { recursive: true });
|
|
6661
|
-
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(
|
|
7557
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
6662
7558
|
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
|
|
6666
|
-
name: "worker-stop-helper-fail",
|
|
6667
|
-
tmux_session: "omx-team-worker-stop",
|
|
6668
|
-
leader_pane_id: "%42",
|
|
6669
|
-
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
6670
|
-
});
|
|
6671
|
-
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
|
|
6672
|
-
name: "worker-stop-helper-fail",
|
|
7559
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7560
|
+
name: teamName,
|
|
6673
7561
|
tmux_session: "omx-team-worker-stop",
|
|
6674
7562
|
leader_pane_id: "%42",
|
|
6675
|
-
workers: [
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
assigned_tasks: ["1"],
|
|
6680
|
-
team_state_root: stateDir,
|
|
6681
|
-
});
|
|
6682
|
-
await writeJson(join(workerDir, "status.json"), {
|
|
6683
|
-
state: "done",
|
|
6684
|
-
current_task_id: "1",
|
|
6685
|
-
updated_at: new Date().toISOString(),
|
|
6686
|
-
});
|
|
6687
|
-
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
|
|
6688
|
-
id: "1",
|
|
6689
|
-
status: "completed",
|
|
6690
|
-
owner: "worker-1",
|
|
7563
|
+
workers: [
|
|
7564
|
+
{ name: "worker-1", index: 1, pane_id: "%10" },
|
|
7565
|
+
{ name: "worker-2", index: 2, pane_id: "%11" },
|
|
7566
|
+
],
|
|
6691
7567
|
});
|
|
6692
|
-
process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
|
|
6693
|
-
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
6694
7568
|
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
6695
|
-
const
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
7569
|
+
const first = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7570
|
+
stateDir,
|
|
7571
|
+
logsDir,
|
|
7572
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7573
|
+
});
|
|
7574
|
+
const second = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7575
|
+
stateDir,
|
|
7576
|
+
logsDir,
|
|
7577
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
7578
|
+
});
|
|
7579
|
+
assert.equal(first.result, "sent");
|
|
7580
|
+
assert.equal(second.result, "suppressed_team_cooldown");
|
|
7581
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7582
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
|
|
7583
|
+
assert.equal(stopNudges.length, 1, "same-team workers should share one leader nudge cooldown window");
|
|
7584
|
+
const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
|
|
7585
|
+
assert.equal(teamNudgeState.worker, "worker-1");
|
|
7586
|
+
assert.equal(teamNudgeState.delivery, "sent");
|
|
6699
7587
|
}
|
|
6700
7588
|
finally {
|
|
6701
|
-
if (typeof prevTeamWorker === "string")
|
|
6702
|
-
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
6703
|
-
else
|
|
6704
|
-
delete process.env.OMX_TEAM_WORKER;
|
|
6705
|
-
if (typeof prevTeamStateRoot === "string")
|
|
6706
|
-
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
6707
|
-
else
|
|
6708
|
-
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
6709
7589
|
if (typeof prevPath === "string")
|
|
6710
7590
|
process.env.PATH = prevPath;
|
|
6711
7591
|
else
|
|
@@ -6713,45 +7593,360 @@ exit 0
|
|
|
6713
7593
|
await rm(cwd, { recursive: true, force: true });
|
|
6714
7594
|
}
|
|
6715
7595
|
});
|
|
6716
|
-
it("
|
|
6717
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-
|
|
6718
|
-
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
6719
|
-
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
6720
|
-
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
7596
|
+
it("serializes concurrent allowed worker Stop leader nudges with a team lock", async () => {
|
|
7597
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-concurrent-dedupe-"));
|
|
6721
7598
|
const prevPath = process.env.PATH;
|
|
6722
7599
|
try {
|
|
6723
|
-
|
|
7600
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7601
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7602
|
+
const teamName = "worker-stop-concurrent";
|
|
7603
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
6724
7604
|
const fakeBinDir = join(cwd, "fake-bin");
|
|
6725
7605
|
const tmuxLogPath = join(cwd, "tmux.log");
|
|
6726
7606
|
await mkdir(fakeBinDir, { recursive: true });
|
|
6727
|
-
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
7607
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { sendDelayMs: 100 }));
|
|
6728
7608
|
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
|
|
6732
|
-
name: "worker-stop-failed-task",
|
|
7609
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7610
|
+
name: teamName,
|
|
6733
7611
|
tmux_session: "omx-team-worker-stop",
|
|
6734
7612
|
leader_pane_id: "%42",
|
|
6735
|
-
workers: [
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
assigned_tasks: ["1"],
|
|
6740
|
-
team_state_root: stateDir,
|
|
6741
|
-
});
|
|
6742
|
-
await writeJson(join(workerDir, "status.json"), {
|
|
6743
|
-
state: "failed",
|
|
6744
|
-
current_task_id: "1",
|
|
6745
|
-
updated_at: new Date().toISOString(),
|
|
6746
|
-
});
|
|
6747
|
-
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
|
|
6748
|
-
id: "1",
|
|
6749
|
-
status: "failed",
|
|
6750
|
-
owner: "worker-1",
|
|
7613
|
+
workers: [
|
|
7614
|
+
{ name: "worker-1", index: 1, pane_id: "%10" },
|
|
7615
|
+
{ name: "worker-2", index: 2, pane_id: "%11" },
|
|
7616
|
+
],
|
|
6751
7617
|
});
|
|
6752
|
-
process.env.
|
|
6753
|
-
|
|
6754
|
-
|
|
7618
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7619
|
+
const results = await Promise.all([
|
|
7620
|
+
maybeNudgeLeaderForAllowedWorkerStop({
|
|
7621
|
+
stateDir,
|
|
7622
|
+
logsDir,
|
|
7623
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7624
|
+
}),
|
|
7625
|
+
maybeNudgeLeaderForAllowedWorkerStop({
|
|
7626
|
+
stateDir,
|
|
7627
|
+
logsDir,
|
|
7628
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
7629
|
+
}),
|
|
7630
|
+
]);
|
|
7631
|
+
assert.equal(results.filter((result) => result.result === "sent").length, 1);
|
|
7632
|
+
assert.equal(results.filter((result) => result.result === "suppressed_team_lock_held").length, 1);
|
|
7633
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7634
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
|
|
7635
|
+
assert.equal(stopNudges.length, 1, "concurrent same-team workers should emit only one leader nudge");
|
|
7636
|
+
assert.equal(existsSync(join(teamDir, "worker-stop-nudge.lock")), false);
|
|
7637
|
+
}
|
|
7638
|
+
finally {
|
|
7639
|
+
if (typeof prevPath === "string")
|
|
7640
|
+
process.env.PATH = prevPath;
|
|
7641
|
+
else
|
|
7642
|
+
delete process.env.PATH;
|
|
7643
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7644
|
+
}
|
|
7645
|
+
});
|
|
7646
|
+
it("skips worker Stop leader nudge when team state is missing or shut down", async () => {
|
|
7647
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-team-"));
|
|
7648
|
+
try {
|
|
7649
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7650
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7651
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7652
|
+
stateDir,
|
|
7653
|
+
logsDir,
|
|
7654
|
+
workerContext: { teamName: "removed-team", workerName: "worker-1" },
|
|
7655
|
+
});
|
|
7656
|
+
assert.equal(result.result, "team_state_gone_or_shutdown");
|
|
7657
|
+
assert.equal(existsSync(join(stateDir, "team", "removed-team", "worker-stop-nudge.json")), false);
|
|
7658
|
+
await writeJson(join(stateDir, "team", "shutdown-team", "shutdown.json"), {
|
|
7659
|
+
started_at: new Date().toISOString(),
|
|
7660
|
+
});
|
|
7661
|
+
const shutdownResult = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7662
|
+
stateDir,
|
|
7663
|
+
logsDir,
|
|
7664
|
+
workerContext: { teamName: "shutdown-team", workerName: "worker-1" },
|
|
7665
|
+
});
|
|
7666
|
+
assert.equal(shutdownResult.result, "team_state_gone_or_shutdown");
|
|
7667
|
+
assert.equal(existsSync(join(stateDir, "team", "shutdown-team", "worker-stop-nudge.json")), false);
|
|
7668
|
+
}
|
|
7669
|
+
finally {
|
|
7670
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7671
|
+
}
|
|
7672
|
+
});
|
|
7673
|
+
it("does not treat old visible worker Stop transcript as pending queue state", async () => {
|
|
7674
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-queue-dedupe-"));
|
|
7675
|
+
const prevPath = process.env.PATH;
|
|
7676
|
+
try {
|
|
7677
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7678
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7679
|
+
const teamName = "queued-stop-dedupe";
|
|
7680
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7681
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7682
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7683
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7684
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
|
|
7685
|
+
busyLeader: true,
|
|
7686
|
+
captureText: `[OMX] worker-1 native Stop allowed. Run \`omx team status ${teamName}\`, read worker messages/results, then assign next task, reconcile completion, or shut down. [OMX_TMUX_INJECT]\n`
|
|
7687
|
+
+ "• Working… (esc to interrupt)",
|
|
7688
|
+
}));
|
|
7689
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7690
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7691
|
+
name: teamName,
|
|
7692
|
+
tmux_session: "omx-team-worker-stop",
|
|
7693
|
+
leader_pane_id: "%42",
|
|
7694
|
+
workers: [{ name: "worker-2", index: 2, pane_id: "%11" }],
|
|
7695
|
+
});
|
|
7696
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7697
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7698
|
+
stateDir,
|
|
7699
|
+
logsDir,
|
|
7700
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
7701
|
+
});
|
|
7702
|
+
assert.equal(result.result, "queued");
|
|
7703
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7704
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-2 native Stop allowed/);
|
|
7705
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
7706
|
+
const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
|
|
7707
|
+
assert.equal(teamNudgeState.worker, "worker-2");
|
|
7708
|
+
assert.equal(teamNudgeState.delivery, "queued");
|
|
7709
|
+
}
|
|
7710
|
+
finally {
|
|
7711
|
+
if (typeof prevPath === "string")
|
|
7712
|
+
process.env.PATH = prevPath;
|
|
7713
|
+
else
|
|
7714
|
+
delete process.env.PATH;
|
|
7715
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7716
|
+
}
|
|
7717
|
+
});
|
|
7718
|
+
it("reports deferred when non-teardown persistence failure prevents worker Stop nudge cooldown state", async () => {
|
|
7719
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-persist-fail-"));
|
|
7720
|
+
const prevPath = process.env.PATH;
|
|
7721
|
+
try {
|
|
7722
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7723
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7724
|
+
const teamName = "worker-stop-persist-fail";
|
|
7725
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7726
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7727
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7728
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7729
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7730
|
+
name: teamName,
|
|
7731
|
+
tmux_session: "omx-team-worker-stop",
|
|
7732
|
+
leader_pane_id: "%42",
|
|
7733
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7734
|
+
});
|
|
7735
|
+
await writeFile(join(teamDir, "workers"), "not a directory");
|
|
7736
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
7737
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7738
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7739
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7740
|
+
stateDir,
|
|
7741
|
+
logsDir,
|
|
7742
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7743
|
+
});
|
|
7744
|
+
assert.equal(result.result, "deferred");
|
|
7745
|
+
assert.equal(existsSync(join(teamDir, "worker-stop-nudge.json")), false);
|
|
7746
|
+
assert.equal(existsSync(join(teamDir, "workers", "worker-1", "worker-stop-nudge.json")), false);
|
|
7747
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7748
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
7749
|
+
const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
7750
|
+
const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
|
|
7751
|
+
.trim()
|
|
7752
|
+
.split("\n")
|
|
7753
|
+
.map((line) => JSON.parse(line));
|
|
7754
|
+
const deferredEvent = deliveryEvents.find((event) => event.event === "nudge_triggered" && event.result === "deferred");
|
|
7755
|
+
assert.equal(deferredEvent?.team, teamName);
|
|
7756
|
+
assert.equal(deferredEvent?.from_worker, "worker-1");
|
|
7757
|
+
assert.match(String(deferredEvent?.reason || ""), /EEXIST|ENOTDIR|not a directory|file already exists/);
|
|
7758
|
+
}
|
|
7759
|
+
finally {
|
|
7760
|
+
if (typeof prevPath === "string")
|
|
7761
|
+
process.env.PATH = prevPath;
|
|
7762
|
+
else
|
|
7763
|
+
delete process.env.PATH;
|
|
7764
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7765
|
+
}
|
|
7766
|
+
});
|
|
7767
|
+
it("does not recreate team state when teardown removes it during worker Stop delivery", async () => {
|
|
7768
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-teardown-race-"));
|
|
7769
|
+
const prevPath = process.env.PATH;
|
|
7770
|
+
try {
|
|
7771
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7772
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7773
|
+
const teamName = "worker-stop-teardown-race";
|
|
7774
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7775
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7776
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7777
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7778
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7779
|
+
name: teamName,
|
|
7780
|
+
tmux_session: "omx-team-worker-stop",
|
|
7781
|
+
leader_pane_id: "%42",
|
|
7782
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7783
|
+
});
|
|
7784
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { removePathOnSend: teamDir }));
|
|
7785
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7786
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7787
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7788
|
+
stateDir,
|
|
7789
|
+
logsDir,
|
|
7790
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7791
|
+
});
|
|
7792
|
+
assert.equal(result.result, "sent");
|
|
7793
|
+
assert.equal(existsSync(teamDir), false, "worker Stop delivery must not recreate removed team state");
|
|
7794
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7795
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
7796
|
+
}
|
|
7797
|
+
finally {
|
|
7798
|
+
if (typeof prevPath === "string")
|
|
7799
|
+
process.env.PATH = prevPath;
|
|
7800
|
+
else
|
|
7801
|
+
delete process.env.PATH;
|
|
7802
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7803
|
+
}
|
|
7804
|
+
});
|
|
7805
|
+
it("does not recreate team state when teardown removes it before deferred worker Stop recording", async () => {
|
|
7806
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-deferred-teardown-"));
|
|
7807
|
+
const prevPath = process.env.PATH;
|
|
7808
|
+
try {
|
|
7809
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7810
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7811
|
+
const teamName = "worker-stop-deferred-teardown";
|
|
7812
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7813
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7814
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7815
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7816
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7817
|
+
name: teamName,
|
|
7818
|
+
tmux_session: "omx-team-worker-stop",
|
|
7819
|
+
leader_pane_id: "%42",
|
|
7820
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7821
|
+
});
|
|
7822
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
|
|
7823
|
+
currentCommand: "bash",
|
|
7824
|
+
captureText: "$ ",
|
|
7825
|
+
removePathOnCapture: teamDir,
|
|
7826
|
+
}));
|
|
7827
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7828
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7829
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7830
|
+
stateDir,
|
|
7831
|
+
logsDir,
|
|
7832
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7833
|
+
});
|
|
7834
|
+
assert.equal(result.result, "team_state_gone_or_shutdown");
|
|
7835
|
+
assert.equal(existsSync(teamDir), false, "deferred worker Stop recording must not recreate removed team state");
|
|
7836
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7837
|
+
assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
7838
|
+
}
|
|
7839
|
+
finally {
|
|
7840
|
+
if (typeof prevPath === "string")
|
|
7841
|
+
process.env.PATH = prevPath;
|
|
7842
|
+
else
|
|
7843
|
+
delete process.env.PATH;
|
|
7844
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7845
|
+
}
|
|
7846
|
+
});
|
|
7847
|
+
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
7848
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
7849
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
7850
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
7851
|
+
const prevPath = process.env.PATH;
|
|
7852
|
+
try {
|
|
7853
|
+
await initTeamState("worker-stop-helper-fail", "worker stop helper failure", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" });
|
|
7854
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7855
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7856
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
|
|
7857
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7858
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7859
|
+
const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
|
|
7860
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
|
|
7861
|
+
name: "worker-stop-helper-fail",
|
|
7862
|
+
tmux_session: "omx-team-worker-stop",
|
|
7863
|
+
leader_pane_id: "%42",
|
|
7864
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7865
|
+
});
|
|
7866
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
|
|
7867
|
+
name: "worker-stop-helper-fail",
|
|
7868
|
+
tmux_session: "omx-team-worker-stop",
|
|
7869
|
+
leader_pane_id: "%42",
|
|
7870
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7871
|
+
});
|
|
7872
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
7873
|
+
name: "worker-1",
|
|
7874
|
+
assigned_tasks: ["1"],
|
|
7875
|
+
team_state_root: stateDir,
|
|
7876
|
+
});
|
|
7877
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
7878
|
+
state: "done",
|
|
7879
|
+
current_task_id: "1",
|
|
7880
|
+
updated_at: new Date().toISOString(),
|
|
7881
|
+
});
|
|
7882
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
|
|
7883
|
+
id: "1",
|
|
7884
|
+
status: "completed",
|
|
7885
|
+
owner: "worker-1",
|
|
7886
|
+
});
|
|
7887
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
|
|
7888
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
7889
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7890
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" }, { cwd });
|
|
7891
|
+
assert.equal(result.outputJson, null);
|
|
7892
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
7893
|
+
assert.equal(nudgeState.delivery, "deferred");
|
|
7894
|
+
}
|
|
7895
|
+
finally {
|
|
7896
|
+
if (typeof prevTeamWorker === "string")
|
|
7897
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
7898
|
+
else
|
|
7899
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
7900
|
+
if (typeof prevTeamStateRoot === "string")
|
|
7901
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
7902
|
+
else
|
|
7903
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
7904
|
+
if (typeof prevPath === "string")
|
|
7905
|
+
process.env.PATH = prevPath;
|
|
7906
|
+
else
|
|
7907
|
+
delete process.env.PATH;
|
|
7908
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7909
|
+
}
|
|
7910
|
+
});
|
|
7911
|
+
it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
|
|
7912
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
|
|
7913
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
7914
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
7915
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
7916
|
+
const prevPath = process.env.PATH;
|
|
7917
|
+
try {
|
|
7918
|
+
await initTeamState("worker-stop-failed-task", "worker stop failed task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" });
|
|
7919
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7920
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7921
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7922
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
7923
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7924
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7925
|
+
const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
|
|
7926
|
+
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
|
|
7927
|
+
name: "worker-stop-failed-task",
|
|
7928
|
+
tmux_session: "omx-team-worker-stop",
|
|
7929
|
+
leader_pane_id: "%42",
|
|
7930
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7931
|
+
});
|
|
7932
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
7933
|
+
name: "worker-1",
|
|
7934
|
+
assigned_tasks: ["1"],
|
|
7935
|
+
team_state_root: stateDir,
|
|
7936
|
+
});
|
|
7937
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
7938
|
+
state: "failed",
|
|
7939
|
+
current_task_id: "1",
|
|
7940
|
+
updated_at: new Date().toISOString(),
|
|
7941
|
+
});
|
|
7942
|
+
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
|
|
7943
|
+
id: "1",
|
|
7944
|
+
status: "failed",
|
|
7945
|
+
owner: "worker-1",
|
|
7946
|
+
});
|
|
7947
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
|
|
7948
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
7949
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
6755
7950
|
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
6756
7951
|
const result = await dispatchCodexNativeHook({
|
|
6757
7952
|
hook_event_name: "Stop",
|
|
@@ -10150,188 +11345,943 @@ exit 0
|
|
|
10150
11345
|
session_id: "sess-stop-team-refire",
|
|
10151
11346
|
thread_id: "thread-stop-team-refire",
|
|
10152
11347
|
});
|
|
10153
|
-
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
10154
|
-
current_phase: "team-verify",
|
|
10155
|
-
max_fix_attempts: 3,
|
|
10156
|
-
current_fix_attempt: 0,
|
|
10157
|
-
transitions: [],
|
|
10158
|
-
updated_at: new Date().toISOString(),
|
|
11348
|
+
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
11349
|
+
current_phase: "team-verify",
|
|
11350
|
+
max_fix_attempts: 3,
|
|
11351
|
+
current_fix_attempt: 0,
|
|
11352
|
+
transitions: [],
|
|
11353
|
+
updated_at: new Date().toISOString(),
|
|
11354
|
+
});
|
|
11355
|
+
await dispatchCodexNativeHook({
|
|
11356
|
+
hook_event_name: "Stop",
|
|
11357
|
+
cwd,
|
|
11358
|
+
session_id: "sess-stop-team-refire",
|
|
11359
|
+
thread_id: "thread-stop-team-refire",
|
|
11360
|
+
turn_id: "turn-stop-team-refire-1",
|
|
11361
|
+
}, { cwd });
|
|
11362
|
+
const result = await dispatchCodexNativeHook({
|
|
11363
|
+
hook_event_name: "Stop",
|
|
11364
|
+
cwd,
|
|
11365
|
+
session_id: "sess-stop-team-refire",
|
|
11366
|
+
thread_id: "thread-stop-team-refire",
|
|
11367
|
+
turn_id: "turn-stop-team-refire-2",
|
|
11368
|
+
stop_hook_active: true,
|
|
11369
|
+
}, { cwd });
|
|
11370
|
+
assert.equal(result.omxEventName, "stop");
|
|
11371
|
+
assert.deepEqual(result.outputJson, {
|
|
11372
|
+
decision: "block",
|
|
11373
|
+
reason: `OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
11374
|
+
stopReason: "team_team-verify",
|
|
11375
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
11376
|
+
});
|
|
11377
|
+
}
|
|
11378
|
+
finally {
|
|
11379
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11380
|
+
}
|
|
11381
|
+
});
|
|
11382
|
+
it("suppresses duplicate team Stop replays across native/canonical session-id drift", async () => {
|
|
11383
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-drift-"));
|
|
11384
|
+
try {
|
|
11385
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11386
|
+
await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
|
|
11387
|
+
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
11388
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
11389
|
+
session_id: "omx-canonical",
|
|
11390
|
+
native_session_id: "codex-native",
|
|
11391
|
+
});
|
|
11392
|
+
await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
|
|
11393
|
+
active: true,
|
|
11394
|
+
current_phase: "starting",
|
|
11395
|
+
team_name: "current-team",
|
|
11396
|
+
session_id: "omx-canonical",
|
|
11397
|
+
});
|
|
11398
|
+
await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
|
|
11399
|
+
current_phase: "team-verify",
|
|
11400
|
+
max_fix_attempts: 3,
|
|
11401
|
+
current_fix_attempt: 1,
|
|
11402
|
+
transitions: [],
|
|
11403
|
+
updated_at: new Date().toISOString(),
|
|
11404
|
+
});
|
|
11405
|
+
await dispatchCodexNativeHook({
|
|
11406
|
+
hook_event_name: "Stop",
|
|
11407
|
+
cwd,
|
|
11408
|
+
session_id: "codex-native",
|
|
11409
|
+
thread_id: "thread-stop-team-drift",
|
|
11410
|
+
turn_id: "turn-stop-team-drift-1",
|
|
11411
|
+
}, { cwd });
|
|
11412
|
+
const duplicate = await dispatchCodexNativeHook({
|
|
11413
|
+
hook_event_name: "Stop",
|
|
11414
|
+
cwd,
|
|
11415
|
+
session_id: "omx-canonical",
|
|
11416
|
+
thread_id: "thread-stop-team-drift",
|
|
11417
|
+
turn_id: "turn-stop-team-drift-1",
|
|
11418
|
+
stop_hook_active: true,
|
|
11419
|
+
}, { cwd });
|
|
11420
|
+
assert.equal(duplicate.omxEventName, "stop");
|
|
11421
|
+
assert.deepEqual(duplicate.outputJson, {
|
|
11422
|
+
decision: "block",
|
|
11423
|
+
reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
11424
|
+
stopReason: "team_team-verify",
|
|
11425
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
11426
|
+
});
|
|
11427
|
+
const fresh = await dispatchCodexNativeHook({
|
|
11428
|
+
hook_event_name: "Stop",
|
|
11429
|
+
cwd,
|
|
11430
|
+
session_id: "omx-canonical",
|
|
11431
|
+
thread_id: "thread-stop-team-drift",
|
|
11432
|
+
turn_id: "turn-stop-team-drift-2",
|
|
11433
|
+
stop_hook_active: true,
|
|
11434
|
+
}, { cwd });
|
|
11435
|
+
assert.equal(fresh.omxEventName, "stop");
|
|
11436
|
+
assert.deepEqual(fresh.outputJson, {
|
|
11437
|
+
decision: "block",
|
|
11438
|
+
reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
11439
|
+
stopReason: "team_team-verify",
|
|
11440
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
11441
|
+
});
|
|
11442
|
+
const persisted = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
|
|
11443
|
+
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
11444
|
+
}
|
|
11445
|
+
finally {
|
|
11446
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11447
|
+
}
|
|
11448
|
+
});
|
|
11449
|
+
it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
|
|
11450
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
|
|
11451
|
+
try {
|
|
11452
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11453
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
|
|
11454
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
|
|
11455
|
+
active: true,
|
|
11456
|
+
current_phase: "executing",
|
|
11457
|
+
});
|
|
11458
|
+
const first = await dispatchCodexNativeHook({
|
|
11459
|
+
hook_event_name: "Stop",
|
|
11460
|
+
cwd,
|
|
11461
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
11462
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
11463
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
11464
|
+
}, { cwd });
|
|
11465
|
+
const repeated = await dispatchCodexNativeHook({
|
|
11466
|
+
hook_event_name: "Stop",
|
|
11467
|
+
cwd,
|
|
11468
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
11469
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
11470
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
11471
|
+
stop_hook_active: true,
|
|
11472
|
+
}, { cwd });
|
|
11473
|
+
const fresh = await dispatchCodexNativeHook({
|
|
11474
|
+
hook_event_name: "Stop",
|
|
11475
|
+
cwd,
|
|
11476
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
11477
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
11478
|
+
turn_id: "turn-stop-ultrawork-repeat-2",
|
|
11479
|
+
stop_hook_active: true,
|
|
11480
|
+
}, { cwd });
|
|
11481
|
+
assert.equal(first.omxEventName, "stop");
|
|
11482
|
+
assert.deepEqual(repeated.outputJson, null);
|
|
11483
|
+
assert.equal(fresh.omxEventName, "stop");
|
|
11484
|
+
assert.deepEqual(fresh.outputJson, {
|
|
11485
|
+
decision: "block",
|
|
11486
|
+
reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
11487
|
+
stopReason: "ultrawork_executing",
|
|
11488
|
+
systemMessage: "OMX ultrawork is still active (phase: executing).",
|
|
11489
|
+
});
|
|
11490
|
+
}
|
|
11491
|
+
finally {
|
|
11492
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11493
|
+
}
|
|
11494
|
+
});
|
|
11495
|
+
it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
|
|
11496
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
|
|
11497
|
+
try {
|
|
11498
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11499
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
|
|
11500
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
|
|
11501
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
|
|
11502
|
+
active: true,
|
|
11503
|
+
skill: "ralplan",
|
|
11504
|
+
phase: "planning",
|
|
11505
|
+
});
|
|
11506
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
|
|
11507
|
+
active: true,
|
|
11508
|
+
current_phase: "planning",
|
|
11509
|
+
});
|
|
11510
|
+
await dispatchCodexNativeHook({
|
|
11511
|
+
hook_event_name: "Stop",
|
|
11512
|
+
cwd,
|
|
11513
|
+
session_id: "sess-stop-skill-repeat",
|
|
11514
|
+
thread_id: "thread-stop-skill-repeat",
|
|
11515
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
11516
|
+
}, { cwd });
|
|
11517
|
+
const repeated = await dispatchCodexNativeHook({
|
|
11518
|
+
hook_event_name: "Stop",
|
|
11519
|
+
cwd,
|
|
11520
|
+
session_id: "sess-stop-skill-repeat",
|
|
11521
|
+
thread_id: "thread-stop-skill-repeat",
|
|
11522
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
11523
|
+
stop_hook_active: true,
|
|
11524
|
+
}, { cwd });
|
|
11525
|
+
assert.equal(repeated.omxEventName, "stop");
|
|
11526
|
+
assert.equal(repeated.outputJson?.decision, "block");
|
|
11527
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
11528
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
11529
|
+
assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
11530
|
+
}
|
|
11531
|
+
finally {
|
|
11532
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11533
|
+
}
|
|
11534
|
+
});
|
|
11535
|
+
it("blocks implementation writes while ralplan is active without execution handoff", async () => {
|
|
11536
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-block-"));
|
|
11537
|
+
try {
|
|
11538
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11539
|
+
const sessionId = "sess-ralplan-pretool-block";
|
|
11540
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11541
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11542
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11543
|
+
active: true,
|
|
11544
|
+
skill: "ralplan",
|
|
11545
|
+
phase: "planning",
|
|
11546
|
+
session_id: sessionId,
|
|
11547
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
11548
|
+
});
|
|
11549
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11550
|
+
active: true,
|
|
11551
|
+
mode: "ralplan",
|
|
11552
|
+
current_phase: "critic-review",
|
|
11553
|
+
session_id: sessionId,
|
|
11554
|
+
});
|
|
11555
|
+
const result = await dispatchCodexNativeHook({
|
|
11556
|
+
hook_event_name: "PreToolUse",
|
|
11557
|
+
cwd,
|
|
11558
|
+
session_id: sessionId,
|
|
11559
|
+
thread_id: "thread-ralplan-pretool-block",
|
|
11560
|
+
tool_name: "Edit",
|
|
11561
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11562
|
+
}, { cwd });
|
|
11563
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11564
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11565
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11566
|
+
assert.match(String(result.outputJson?.hookSpecificOutput?.additionalContext ?? ""), /\$ultragoal.*\$team.*\$ralph/i);
|
|
11567
|
+
}
|
|
11568
|
+
finally {
|
|
11569
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11570
|
+
}
|
|
11571
|
+
});
|
|
11572
|
+
it("blocks implementation writes while Autopilot is supervising ralplan without handoff", async () => {
|
|
11573
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-block-"));
|
|
11574
|
+
try {
|
|
11575
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11576
|
+
const sessionId = "sess-autopilot-ralplan-pretool-block";
|
|
11577
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11578
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11579
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11580
|
+
active: true,
|
|
11581
|
+
skill: "autopilot",
|
|
11582
|
+
phase: "ralplan",
|
|
11583
|
+
session_id: sessionId,
|
|
11584
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
11585
|
+
});
|
|
11586
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11587
|
+
active: true,
|
|
11588
|
+
mode: "autopilot",
|
|
11589
|
+
current_phase: "ralplan",
|
|
11590
|
+
session_id: sessionId,
|
|
11591
|
+
state: {
|
|
11592
|
+
handoff_artifacts: {
|
|
11593
|
+
ralplan_consensus_gate: { required: true, complete: false },
|
|
11594
|
+
},
|
|
11595
|
+
},
|
|
11596
|
+
});
|
|
11597
|
+
const result = await dispatchCodexNativeHook({
|
|
11598
|
+
hook_event_name: "PreToolUse",
|
|
11599
|
+
cwd,
|
|
11600
|
+
session_id: sessionId,
|
|
11601
|
+
thread_id: "thread-autopilot-ralplan-pretool-block",
|
|
11602
|
+
tool_name: "Edit",
|
|
11603
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11604
|
+
}, { cwd });
|
|
11605
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11606
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11607
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11608
|
+
}
|
|
11609
|
+
finally {
|
|
11610
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11611
|
+
}
|
|
11612
|
+
});
|
|
11613
|
+
it("does not block implementation writes from Autopilot ralplan detail state without canonical skill state", async () => {
|
|
11614
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-no-canonical-"));
|
|
11615
|
+
try {
|
|
11616
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11617
|
+
const sessionId = "sess-autopilot-ralplan-no-canonical";
|
|
11618
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11619
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11620
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11621
|
+
active: true,
|
|
11622
|
+
mode: "autopilot",
|
|
11623
|
+
current_phase: "ralplan",
|
|
11624
|
+
session_id: sessionId,
|
|
11625
|
+
});
|
|
11626
|
+
const result = await dispatchCodexNativeHook({
|
|
11627
|
+
hook_event_name: "PreToolUse",
|
|
11628
|
+
cwd,
|
|
11629
|
+
session_id: sessionId,
|
|
11630
|
+
thread_id: "thread-autopilot-ralplan-no-canonical",
|
|
11631
|
+
tool_name: "Edit",
|
|
11632
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11633
|
+
}, { cwd });
|
|
11634
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11635
|
+
assert.equal(result.outputJson, null);
|
|
11636
|
+
}
|
|
11637
|
+
finally {
|
|
11638
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11639
|
+
}
|
|
11640
|
+
});
|
|
11641
|
+
it("allows implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
|
|
11642
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-terminal-pretool-"));
|
|
11643
|
+
try {
|
|
11644
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11645
|
+
const sessionId = "sess-autopilot-ralplan-terminal-pretool";
|
|
11646
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11647
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11648
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11649
|
+
active: true,
|
|
11650
|
+
skill: "autopilot",
|
|
11651
|
+
phase: "ralplan",
|
|
11652
|
+
session_id: sessionId,
|
|
11653
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
11654
|
+
});
|
|
11655
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11656
|
+
active: true,
|
|
11657
|
+
mode: "autopilot",
|
|
11658
|
+
current_phase: "ralplan",
|
|
11659
|
+
session_id: sessionId,
|
|
11660
|
+
});
|
|
11661
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
11662
|
+
version: 1,
|
|
11663
|
+
active: false,
|
|
11664
|
+
mode: "autopilot",
|
|
11665
|
+
outcome: "finish",
|
|
11666
|
+
lifecycle_outcome: "finished",
|
|
11667
|
+
current_phase: "complete",
|
|
11668
|
+
completed_at: "2026-05-30T00:00:00.000Z",
|
|
11669
|
+
updated_at: "2026-05-30T00:00:00.000Z",
|
|
11670
|
+
});
|
|
11671
|
+
const result = await dispatchCodexNativeHook({
|
|
11672
|
+
hook_event_name: "PreToolUse",
|
|
11673
|
+
cwd,
|
|
11674
|
+
session_id: sessionId,
|
|
11675
|
+
thread_id: "thread-autopilot-ralplan-terminal-pretool",
|
|
11676
|
+
tool_name: "Edit",
|
|
11677
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11678
|
+
}, { cwd });
|
|
11679
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11680
|
+
assert.equal(result.outputJson, null);
|
|
11681
|
+
}
|
|
11682
|
+
finally {
|
|
11683
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11684
|
+
}
|
|
11685
|
+
});
|
|
11686
|
+
it("blocks bash implementation writes while Autopilot is supervising ralplan without handoff", async () => {
|
|
11687
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-bash-block-"));
|
|
11688
|
+
try {
|
|
11689
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11690
|
+
const sessionId = "sess-autopilot-ralplan-pretool-bash-block";
|
|
11691
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11692
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11693
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11694
|
+
active: true,
|
|
11695
|
+
skill: "autopilot",
|
|
11696
|
+
phase: "ralplan",
|
|
11697
|
+
session_id: sessionId,
|
|
11698
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
11699
|
+
});
|
|
11700
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11701
|
+
active: true,
|
|
11702
|
+
mode: "autopilot",
|
|
11703
|
+
current_phase: "ralplan",
|
|
11704
|
+
session_id: sessionId,
|
|
11705
|
+
});
|
|
11706
|
+
const result = await dispatchCodexNativeHook({
|
|
11707
|
+
hook_event_name: "PreToolUse",
|
|
11708
|
+
cwd,
|
|
11709
|
+
session_id: sessionId,
|
|
11710
|
+
thread_id: "thread-autopilot-ralplan-pretool-bash-block",
|
|
11711
|
+
tool_name: "Bash",
|
|
11712
|
+
tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
|
|
11713
|
+
}, { cwd });
|
|
11714
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11715
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11716
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11717
|
+
}
|
|
11718
|
+
finally {
|
|
11719
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11720
|
+
}
|
|
11721
|
+
});
|
|
11722
|
+
it("blocks implementation writes when ralplan and Autopilot ralplan are both active", async () => {
|
|
11723
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-autopilot-mixed-planning-"));
|
|
11724
|
+
try {
|
|
11725
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11726
|
+
const sessionId = "sess-ralplan-autopilot-mixed-planning";
|
|
11727
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11728
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11729
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11730
|
+
active: true,
|
|
11731
|
+
skill: "autopilot",
|
|
11732
|
+
phase: "ralplan",
|
|
11733
|
+
session_id: sessionId,
|
|
11734
|
+
active_skills: [
|
|
11735
|
+
{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
|
|
11736
|
+
{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId },
|
|
11737
|
+
],
|
|
11738
|
+
});
|
|
11739
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11740
|
+
active: true,
|
|
11741
|
+
mode: "ralplan",
|
|
11742
|
+
current_phase: "planning",
|
|
11743
|
+
session_id: sessionId,
|
|
11744
|
+
});
|
|
11745
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11746
|
+
active: true,
|
|
11747
|
+
mode: "autopilot",
|
|
11748
|
+
current_phase: "ralplan",
|
|
11749
|
+
session_id: sessionId,
|
|
11750
|
+
});
|
|
11751
|
+
const result = await dispatchCodexNativeHook({
|
|
11752
|
+
hook_event_name: "PreToolUse",
|
|
11753
|
+
cwd,
|
|
11754
|
+
session_id: sessionId,
|
|
11755
|
+
thread_id: "thread-ralplan-autopilot-mixed-planning",
|
|
11756
|
+
tool_name: "Edit",
|
|
11757
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11758
|
+
}, { cwd });
|
|
11759
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11760
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11761
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11762
|
+
}
|
|
11763
|
+
finally {
|
|
11764
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11765
|
+
}
|
|
11766
|
+
});
|
|
11767
|
+
it("blocks implementation writes while Autopilot is supervising replan without handoff", async () => {
|
|
11768
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-replan-pretool-block-"));
|
|
11769
|
+
try {
|
|
11770
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11771
|
+
const sessionId = "sess-autopilot-replan-pretool-block";
|
|
11772
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11773
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11774
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11775
|
+
active: true,
|
|
11776
|
+
skill: "autopilot",
|
|
11777
|
+
phase: "replan",
|
|
11778
|
+
session_id: sessionId,
|
|
11779
|
+
active_skills: [{ skill: "autopilot", phase: "replan", active: true, session_id: sessionId }],
|
|
11780
|
+
});
|
|
11781
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11782
|
+
active: true,
|
|
11783
|
+
mode: "autopilot",
|
|
11784
|
+
current_phase: "replan",
|
|
11785
|
+
session_id: sessionId,
|
|
11786
|
+
});
|
|
11787
|
+
const result = await dispatchCodexNativeHook({
|
|
11788
|
+
hook_event_name: "PreToolUse",
|
|
11789
|
+
cwd,
|
|
11790
|
+
session_id: sessionId,
|
|
11791
|
+
thread_id: "thread-autopilot-replan-pretool-block",
|
|
11792
|
+
tool_name: "Edit",
|
|
11793
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11794
|
+
}, { cwd });
|
|
11795
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11796
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11797
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11798
|
+
}
|
|
11799
|
+
finally {
|
|
11800
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11801
|
+
}
|
|
11802
|
+
});
|
|
11803
|
+
it("blocks implementation writes when native Codex id maps to OMX Autopilot ralplan state", async () => {
|
|
11804
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-native-map-block-"));
|
|
11805
|
+
try {
|
|
11806
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11807
|
+
const sessionId = "sess-autopilot-ralplan-native-map-block";
|
|
11808
|
+
const nativeSessionId = "019e-autopilot-ralplan-native";
|
|
11809
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
11810
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
|
|
11811
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11812
|
+
active: true,
|
|
11813
|
+
mode: "autopilot",
|
|
11814
|
+
current_phase: "ralplan",
|
|
11815
|
+
session_id: sessionId,
|
|
11816
|
+
});
|
|
11817
|
+
const result = await dispatchCodexNativeHook({
|
|
11818
|
+
hook_event_name: "PreToolUse",
|
|
11819
|
+
cwd,
|
|
11820
|
+
session_id: nativeSessionId,
|
|
11821
|
+
thread_id: "thread-autopilot-ralplan-native-map-block",
|
|
11822
|
+
tool_name: "apply_patch",
|
|
11823
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11824
|
+
}, { cwd });
|
|
11825
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11826
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11827
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11828
|
+
}
|
|
11829
|
+
finally {
|
|
11830
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11831
|
+
}
|
|
11832
|
+
});
|
|
11833
|
+
it("blocks bash implementation writes when native Codex id maps to OMX Autopilot ralplan state", async () => {
|
|
11834
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-native-map-bash-"));
|
|
11835
|
+
try {
|
|
11836
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11837
|
+
const sessionId = "sess-autopilot-ralplan-native-map-bash";
|
|
11838
|
+
const nativeSessionId = "019e-autopilot-ralplan-native-bash";
|
|
11839
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
11840
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
|
|
11841
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11842
|
+
active: true,
|
|
11843
|
+
mode: "autopilot",
|
|
11844
|
+
current_phase: "ralplan",
|
|
11845
|
+
session_id: sessionId,
|
|
11846
|
+
});
|
|
11847
|
+
const result = await dispatchCodexNativeHook({
|
|
11848
|
+
hook_event_name: "PreToolUse",
|
|
11849
|
+
cwd,
|
|
11850
|
+
session_id: nativeSessionId,
|
|
11851
|
+
thread_id: "thread-autopilot-ralplan-native-map-bash",
|
|
11852
|
+
tool_name: "Bash",
|
|
11853
|
+
tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
|
|
11854
|
+
}, { cwd });
|
|
11855
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11856
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11857
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11858
|
+
}
|
|
11859
|
+
finally {
|
|
11860
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11861
|
+
}
|
|
11862
|
+
});
|
|
11863
|
+
it("blocks standalone ralplan writes when native Codex id maps to OMX session state", async () => {
|
|
11864
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-block-"));
|
|
11865
|
+
try {
|
|
11866
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11867
|
+
const sessionId = "sess-ralplan-native-map-block";
|
|
11868
|
+
const nativeSessionId = "019e-ralplan-native-map";
|
|
11869
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
11870
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
|
|
11871
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11872
|
+
active: true,
|
|
11873
|
+
mode: "ralplan",
|
|
11874
|
+
current_phase: "planning",
|
|
11875
|
+
session_id: sessionId,
|
|
11876
|
+
});
|
|
11877
|
+
const result = await dispatchCodexNativeHook({
|
|
11878
|
+
hook_event_name: "PreToolUse",
|
|
11879
|
+
cwd,
|
|
11880
|
+
session_id: nativeSessionId,
|
|
11881
|
+
thread_id: "thread-ralplan-native-map-block",
|
|
11882
|
+
tool_name: "Edit",
|
|
11883
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11884
|
+
}, { cwd });
|
|
11885
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11886
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11887
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
11888
|
+
}
|
|
11889
|
+
finally {
|
|
11890
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11891
|
+
}
|
|
11892
|
+
});
|
|
11893
|
+
it("blocks deep-interview writes when native Codex id maps to OMX session state", async () => {
|
|
11894
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-native-map-block-"));
|
|
11895
|
+
try {
|
|
11896
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11897
|
+
const sessionId = "sess-deep-interview-native-map-block";
|
|
11898
|
+
const nativeSessionId = "019e-deep-interview-native-map";
|
|
11899
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
11900
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "deep-interview", "interview");
|
|
11901
|
+
await writeJson(join(stateDir, "sessions", sessionId, "deep-interview-state.json"), {
|
|
11902
|
+
active: true,
|
|
11903
|
+
mode: "deep-interview",
|
|
11904
|
+
current_phase: "interview",
|
|
11905
|
+
session_id: sessionId,
|
|
11906
|
+
});
|
|
11907
|
+
const result = await dispatchCodexNativeHook({
|
|
11908
|
+
hook_event_name: "PreToolUse",
|
|
11909
|
+
cwd,
|
|
11910
|
+
session_id: nativeSessionId,
|
|
11911
|
+
thread_id: "thread-deep-interview-native-map-block",
|
|
11912
|
+
tool_name: "Edit",
|
|
11913
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11914
|
+
}, { cwd });
|
|
11915
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11916
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11917
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Deep-interview is active .*implementation\/write tools are blocked/i);
|
|
11918
|
+
}
|
|
11919
|
+
finally {
|
|
11920
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11921
|
+
}
|
|
11922
|
+
});
|
|
11923
|
+
it("allows mapped ralplan planning artifact writes without execution handoff", async () => {
|
|
11924
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-artifact-"));
|
|
11925
|
+
try {
|
|
11926
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11927
|
+
const sessionId = "sess-ralplan-native-map-artifact";
|
|
11928
|
+
const nativeSessionId = "019e-ralplan-native-map-artifact";
|
|
11929
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
11930
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
|
|
11931
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11932
|
+
active: true,
|
|
11933
|
+
mode: "ralplan",
|
|
11934
|
+
current_phase: "planning",
|
|
11935
|
+
session_id: sessionId,
|
|
11936
|
+
});
|
|
11937
|
+
const result = await dispatchCodexNativeHook({
|
|
11938
|
+
hook_event_name: "PreToolUse",
|
|
11939
|
+
cwd,
|
|
11940
|
+
session_id: nativeSessionId,
|
|
11941
|
+
thread_id: "thread-ralplan-native-map-artifact",
|
|
11942
|
+
tool_name: "Bash",
|
|
11943
|
+
tool_input: { command: "cat <<'EOF' > .omx/plans/prd-native-map.md\nplanning\nEOF" },
|
|
11944
|
+
}, { cwd });
|
|
11945
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11946
|
+
assert.equal(result.outputJson, null);
|
|
11947
|
+
}
|
|
11948
|
+
finally {
|
|
11949
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11950
|
+
}
|
|
11951
|
+
});
|
|
11952
|
+
it("allows mapped implementation writes when explicit execution handoff is active", async () => {
|
|
11953
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-handoff-"));
|
|
11954
|
+
try {
|
|
11955
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11956
|
+
const sessionId = "sess-ralplan-native-map-handoff";
|
|
11957
|
+
const nativeSessionId = "019e-ralplan-native-map-handoff";
|
|
11958
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
11959
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11960
|
+
active: true,
|
|
11961
|
+
skill: "ultragoal",
|
|
11962
|
+
phase: "planning",
|
|
11963
|
+
session_id: sessionId,
|
|
11964
|
+
active_skills: [
|
|
11965
|
+
{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
|
|
11966
|
+
{ skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
|
|
11967
|
+
],
|
|
11968
|
+
});
|
|
11969
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11970
|
+
active: true,
|
|
11971
|
+
mode: "ralplan",
|
|
11972
|
+
current_phase: "complete",
|
|
11973
|
+
session_id: sessionId,
|
|
11974
|
+
});
|
|
11975
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
11976
|
+
active: true,
|
|
11977
|
+
mode: "ultragoal",
|
|
11978
|
+
current_phase: "planning",
|
|
11979
|
+
session_id: sessionId,
|
|
11980
|
+
});
|
|
11981
|
+
const result = await dispatchCodexNativeHook({
|
|
11982
|
+
hook_event_name: "PreToolUse",
|
|
11983
|
+
cwd,
|
|
11984
|
+
session_id: nativeSessionId,
|
|
11985
|
+
thread_id: "thread-ralplan-native-map-handoff",
|
|
11986
|
+
tool_name: "Edit",
|
|
11987
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11988
|
+
}, { cwd });
|
|
11989
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11990
|
+
assert.equal(result.outputJson, null);
|
|
11991
|
+
}
|
|
11992
|
+
finally {
|
|
11993
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11994
|
+
}
|
|
11995
|
+
});
|
|
11996
|
+
it("allows mapped implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
|
|
11997
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-native-map-terminal-"));
|
|
11998
|
+
try {
|
|
11999
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12000
|
+
const sessionId = "sess-autopilot-ralplan-native-map-terminal";
|
|
12001
|
+
const nativeSessionId = "019e-autopilot-ralplan-native-terminal";
|
|
12002
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
12003
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
|
|
12004
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
12005
|
+
active: true,
|
|
12006
|
+
mode: "autopilot",
|
|
12007
|
+
current_phase: "ralplan",
|
|
12008
|
+
session_id: sessionId,
|
|
12009
|
+
});
|
|
12010
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
12011
|
+
version: 1,
|
|
12012
|
+
active: false,
|
|
12013
|
+
mode: "autopilot",
|
|
12014
|
+
outcome: "finish",
|
|
12015
|
+
lifecycle_outcome: "finished",
|
|
12016
|
+
current_phase: "complete",
|
|
12017
|
+
completed_at: "2026-05-30T00:00:00.000Z",
|
|
12018
|
+
updated_at: "2026-05-30T00:00:00.000Z",
|
|
12019
|
+
});
|
|
12020
|
+
const result = await dispatchCodexNativeHook({
|
|
12021
|
+
hook_event_name: "PreToolUse",
|
|
12022
|
+
cwd,
|
|
12023
|
+
session_id: nativeSessionId,
|
|
12024
|
+
thread_id: "thread-autopilot-ralplan-native-map-terminal",
|
|
12025
|
+
tool_name: "Edit",
|
|
12026
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12027
|
+
}, { cwd });
|
|
12028
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12029
|
+
assert.equal(result.outputJson, null);
|
|
12030
|
+
}
|
|
12031
|
+
finally {
|
|
12032
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12033
|
+
}
|
|
12034
|
+
});
|
|
12035
|
+
it("does not block unrelated native Codex ids when current OMX session mapping does not match", async () => {
|
|
12036
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-unrelated-"));
|
|
12037
|
+
try {
|
|
12038
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12039
|
+
const sessionId = "sess-ralplan-native-map-owner";
|
|
12040
|
+
const ownerNativeSessionId = "019e-ralplan-native-owner";
|
|
12041
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, ownerNativeSessionId);
|
|
12042
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
|
|
12043
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12044
|
+
active: true,
|
|
12045
|
+
mode: "ralplan",
|
|
12046
|
+
current_phase: "planning",
|
|
12047
|
+
session_id: sessionId,
|
|
12048
|
+
});
|
|
12049
|
+
const result = await dispatchCodexNativeHook({
|
|
12050
|
+
hook_event_name: "PreToolUse",
|
|
12051
|
+
cwd,
|
|
12052
|
+
session_id: "019e-unrelated-native-session",
|
|
12053
|
+
thread_id: "thread-ralplan-native-map-unrelated",
|
|
12054
|
+
tool_name: "Edit",
|
|
12055
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12056
|
+
}, { cwd });
|
|
12057
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12058
|
+
assert.equal(result.outputJson, null);
|
|
12059
|
+
}
|
|
12060
|
+
finally {
|
|
12061
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12062
|
+
}
|
|
12063
|
+
});
|
|
12064
|
+
it("blocks mapped Autopilot ralplan writes from the authoritative team state root", async () => {
|
|
12065
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-team-root-"));
|
|
12066
|
+
const teamStateRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-team-root-"));
|
|
12067
|
+
const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
12068
|
+
try {
|
|
12069
|
+
process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
|
|
12070
|
+
const stateDir = teamStateRoot;
|
|
12071
|
+
const sessionId = "sess-autopilot-ralplan-team-root";
|
|
12072
|
+
const nativeSessionId = "019e-autopilot-ralplan-team-root";
|
|
12073
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
12074
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "autopilot", "ralplan");
|
|
12075
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
12076
|
+
active: true,
|
|
12077
|
+
mode: "autopilot",
|
|
12078
|
+
current_phase: "ralplan",
|
|
12079
|
+
session_id: sessionId,
|
|
12080
|
+
});
|
|
12081
|
+
const result = await dispatchCodexNativeHook({
|
|
12082
|
+
hook_event_name: "PreToolUse",
|
|
12083
|
+
cwd,
|
|
12084
|
+
session_id: nativeSessionId,
|
|
12085
|
+
thread_id: "thread-autopilot-ralplan-team-root",
|
|
12086
|
+
tool_name: "Edit",
|
|
12087
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12088
|
+
}, { cwd });
|
|
12089
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12090
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
12091
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
12092
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "session.json")), false);
|
|
12093
|
+
}
|
|
12094
|
+
finally {
|
|
12095
|
+
if (typeof previousTeamStateRoot === "string")
|
|
12096
|
+
process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
|
|
12097
|
+
else
|
|
12098
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
12099
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12100
|
+
await rm(teamStateRoot, { recursive: true, force: true });
|
|
12101
|
+
}
|
|
12102
|
+
});
|
|
12103
|
+
it("does not block unrelated native Codex ids from the authoritative team state root", async () => {
|
|
12104
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-team-root-unrelated-"));
|
|
12105
|
+
const teamStateRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-team-root-unrelated-"));
|
|
12106
|
+
const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
12107
|
+
try {
|
|
12108
|
+
process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
|
|
12109
|
+
const stateDir = teamStateRoot;
|
|
12110
|
+
const sessionId = "sess-ralplan-team-root-owner";
|
|
12111
|
+
const nativeSessionId = "019e-ralplan-team-root-owner";
|
|
12112
|
+
await writeNativeMappedSessionState(cwd, stateDir, sessionId, nativeSessionId);
|
|
12113
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
|
|
12114
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12115
|
+
active: true,
|
|
12116
|
+
mode: "ralplan",
|
|
12117
|
+
current_phase: "planning",
|
|
12118
|
+
session_id: sessionId,
|
|
12119
|
+
});
|
|
12120
|
+
const result = await dispatchCodexNativeHook({
|
|
12121
|
+
hook_event_name: "PreToolUse",
|
|
12122
|
+
cwd,
|
|
12123
|
+
session_id: "019e-unrelated-team-root-native",
|
|
12124
|
+
thread_id: "thread-ralplan-team-root-unrelated",
|
|
12125
|
+
tool_name: "Edit",
|
|
12126
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12127
|
+
}, { cwd });
|
|
12128
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12129
|
+
assert.equal(result.outputJson, null);
|
|
12130
|
+
}
|
|
12131
|
+
finally {
|
|
12132
|
+
if (typeof previousTeamStateRoot === "string")
|
|
12133
|
+
process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
|
|
12134
|
+
else
|
|
12135
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
12136
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12137
|
+
await rm(teamStateRoot, { recursive: true, force: true });
|
|
12138
|
+
}
|
|
12139
|
+
});
|
|
12140
|
+
it("allows ralplan planning artifact writes without execution handoff", async () => {
|
|
12141
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-artifact-"));
|
|
12142
|
+
try {
|
|
12143
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12144
|
+
const sessionId = "sess-ralplan-pretool-artifact";
|
|
12145
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12146
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12147
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
12148
|
+
active: true,
|
|
12149
|
+
skill: "ralplan",
|
|
12150
|
+
phase: "planning",
|
|
12151
|
+
session_id: sessionId,
|
|
12152
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
12153
|
+
});
|
|
12154
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12155
|
+
active: true,
|
|
12156
|
+
mode: "ralplan",
|
|
12157
|
+
current_phase: "planning",
|
|
12158
|
+
session_id: sessionId,
|
|
10159
12159
|
});
|
|
10160
|
-
await dispatchCodexNativeHook({
|
|
10161
|
-
hook_event_name: "Stop",
|
|
10162
|
-
cwd,
|
|
10163
|
-
session_id: "sess-stop-team-refire",
|
|
10164
|
-
thread_id: "thread-stop-team-refire",
|
|
10165
|
-
turn_id: "turn-stop-team-refire-1",
|
|
10166
|
-
}, { cwd });
|
|
10167
12160
|
const result = await dispatchCodexNativeHook({
|
|
10168
|
-
hook_event_name: "
|
|
12161
|
+
hook_event_name: "PreToolUse",
|
|
10169
12162
|
cwd,
|
|
10170
|
-
session_id:
|
|
10171
|
-
thread_id: "thread-
|
|
10172
|
-
|
|
10173
|
-
|
|
12163
|
+
session_id: sessionId,
|
|
12164
|
+
thread_id: "thread-ralplan-pretool-artifact",
|
|
12165
|
+
tool_name: "Write",
|
|
12166
|
+
tool_input: { file_path: ".omx/plans/prd-issue-2603.md" },
|
|
10174
12167
|
}, { cwd });
|
|
10175
|
-
assert.equal(result.omxEventName, "
|
|
10176
|
-
assert.
|
|
10177
|
-
decision: "block",
|
|
10178
|
-
reason: `OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
10179
|
-
stopReason: "team_team-verify",
|
|
10180
|
-
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
10181
|
-
});
|
|
12168
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12169
|
+
assert.equal(result.outputJson, null);
|
|
10182
12170
|
}
|
|
10183
12171
|
finally {
|
|
10184
12172
|
await rm(cwd, { recursive: true, force: true });
|
|
10185
12173
|
}
|
|
10186
12174
|
});
|
|
10187
|
-
it("
|
|
10188
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
12175
|
+
it("blocks bash implementation writes while ralplan is active without execution handoff", async () => {
|
|
12176
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-block-"));
|
|
10189
12177
|
try {
|
|
10190
12178
|
const stateDir = join(cwd, ".omx", "state");
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
await writeJson(join(stateDir, "session.json"), {
|
|
10194
|
-
|
|
10195
|
-
native_session_id: "codex-native",
|
|
10196
|
-
});
|
|
10197
|
-
await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
|
|
12179
|
+
const sessionId = "sess-ralplan-pretool-bash-block";
|
|
12180
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12181
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12182
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
10198
12183
|
active: true,
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
session_id:
|
|
10202
|
-
|
|
10203
|
-
await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
|
|
10204
|
-
current_phase: "team-verify",
|
|
10205
|
-
max_fix_attempts: 3,
|
|
10206
|
-
current_fix_attempt: 1,
|
|
10207
|
-
transitions: [],
|
|
10208
|
-
updated_at: new Date().toISOString(),
|
|
12184
|
+
skill: "ralplan",
|
|
12185
|
+
phase: "planning",
|
|
12186
|
+
session_id: sessionId,
|
|
12187
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
10209
12188
|
});
|
|
10210
|
-
await
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
turn_id: "turn-stop-team-drift-1",
|
|
10216
|
-
}, { cwd });
|
|
10217
|
-
const duplicate = await dispatchCodexNativeHook({
|
|
10218
|
-
hook_event_name: "Stop",
|
|
10219
|
-
cwd,
|
|
10220
|
-
session_id: "omx-canonical",
|
|
10221
|
-
thread_id: "thread-stop-team-drift",
|
|
10222
|
-
turn_id: "turn-stop-team-drift-1",
|
|
10223
|
-
stop_hook_active: true,
|
|
10224
|
-
}, { cwd });
|
|
10225
|
-
assert.equal(duplicate.omxEventName, "stop");
|
|
10226
|
-
assert.deepEqual(duplicate.outputJson, {
|
|
10227
|
-
decision: "block",
|
|
10228
|
-
reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
10229
|
-
stopReason: "team_team-verify",
|
|
10230
|
-
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
12189
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12190
|
+
active: true,
|
|
12191
|
+
mode: "ralplan",
|
|
12192
|
+
current_phase: "planning",
|
|
12193
|
+
session_id: sessionId,
|
|
10231
12194
|
});
|
|
10232
|
-
const
|
|
10233
|
-
hook_event_name: "
|
|
12195
|
+
const result = await dispatchCodexNativeHook({
|
|
12196
|
+
hook_event_name: "PreToolUse",
|
|
10234
12197
|
cwd,
|
|
10235
|
-
session_id:
|
|
10236
|
-
thread_id: "thread-
|
|
10237
|
-
|
|
10238
|
-
|
|
12198
|
+
session_id: sessionId,
|
|
12199
|
+
thread_id: "thread-ralplan-pretool-bash-block",
|
|
12200
|
+
tool_name: "Bash",
|
|
12201
|
+
tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
|
|
10239
12202
|
}, { cwd });
|
|
10240
|
-
assert.equal(
|
|
10241
|
-
assert.
|
|
10242
|
-
|
|
10243
|
-
reason: `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
10244
|
-
stopReason: "team_team-verify",
|
|
10245
|
-
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
10246
|
-
});
|
|
10247
|
-
const persisted = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
|
|
10248
|
-
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
12203
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12204
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
12205
|
+
assert.match(String(result.outputJson?.reason ?? ""), /(?:Ralplan|Autopilot planning) is active .*implementation\/write tools are blocked/i);
|
|
10249
12206
|
}
|
|
10250
12207
|
finally {
|
|
10251
12208
|
await rm(cwd, { recursive: true, force: true });
|
|
10252
12209
|
}
|
|
10253
12210
|
});
|
|
10254
|
-
it("
|
|
10255
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
12211
|
+
it("allows bash planning artifact writes while ralplan is active without execution handoff", async () => {
|
|
12212
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-artifact-"));
|
|
10256
12213
|
try {
|
|
10257
12214
|
const stateDir = join(cwd, ".omx", "state");
|
|
10258
|
-
|
|
10259
|
-
await
|
|
12215
|
+
const sessionId = "sess-ralplan-pretool-bash-artifact";
|
|
12216
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12217
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12218
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
10260
12219
|
active: true,
|
|
10261
|
-
|
|
12220
|
+
skill: "ralplan",
|
|
12221
|
+
phase: "planning",
|
|
12222
|
+
session_id: sessionId,
|
|
12223
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
10262
12224
|
});
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
hook_event_name: "Stop",
|
|
10272
|
-
cwd,
|
|
10273
|
-
session_id: "sess-stop-ultrawork-repeat",
|
|
10274
|
-
thread_id: "thread-stop-ultrawork-repeat",
|
|
10275
|
-
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
10276
|
-
stop_hook_active: true,
|
|
10277
|
-
}, { cwd });
|
|
10278
|
-
const fresh = await dispatchCodexNativeHook({
|
|
10279
|
-
hook_event_name: "Stop",
|
|
12225
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12226
|
+
active: true,
|
|
12227
|
+
mode: "ralplan",
|
|
12228
|
+
current_phase: "planning",
|
|
12229
|
+
session_id: sessionId,
|
|
12230
|
+
});
|
|
12231
|
+
const result = await dispatchCodexNativeHook({
|
|
12232
|
+
hook_event_name: "PreToolUse",
|
|
10280
12233
|
cwd,
|
|
10281
|
-
session_id:
|
|
10282
|
-
thread_id: "thread-
|
|
10283
|
-
|
|
10284
|
-
|
|
12234
|
+
session_id: sessionId,
|
|
12235
|
+
thread_id: "thread-ralplan-pretool-bash-artifact",
|
|
12236
|
+
tool_name: "Bash",
|
|
12237
|
+
tool_input: { command: "cat <<'EOF' > .omx/plans/prd-issue-2603.md\nplanning\nEOF" },
|
|
10285
12238
|
}, { cwd });
|
|
10286
|
-
assert.equal(
|
|
10287
|
-
assert.
|
|
10288
|
-
assert.equal(fresh.omxEventName, "stop");
|
|
10289
|
-
assert.deepEqual(fresh.outputJson, {
|
|
10290
|
-
decision: "block",
|
|
10291
|
-
reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
10292
|
-
stopReason: "ultrawork_executing",
|
|
10293
|
-
systemMessage: "OMX ultrawork is still active (phase: executing).",
|
|
10294
|
-
});
|
|
12239
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12240
|
+
assert.equal(result.outputJson, null);
|
|
10295
12241
|
}
|
|
10296
12242
|
finally {
|
|
10297
12243
|
await rm(cwd, { recursive: true, force: true });
|
|
10298
12244
|
}
|
|
10299
12245
|
});
|
|
10300
|
-
it("
|
|
10301
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
12246
|
+
it("allows implementation writes when an explicit execution handoff is active", async () => {
|
|
12247
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-handoff-"));
|
|
10302
12248
|
try {
|
|
10303
12249
|
const stateDir = join(cwd, ".omx", "state");
|
|
10304
|
-
|
|
10305
|
-
await
|
|
10306
|
-
await writeJson(join(stateDir, "
|
|
12250
|
+
const sessionId = "sess-ralplan-pretool-handoff";
|
|
12251
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12252
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12253
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
10307
12254
|
active: true,
|
|
10308
|
-
skill: "
|
|
12255
|
+
skill: "ultragoal",
|
|
10309
12256
|
phase: "planning",
|
|
12257
|
+
session_id: sessionId,
|
|
12258
|
+
active_skills: [
|
|
12259
|
+
{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
|
|
12260
|
+
{ skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
|
|
12261
|
+
],
|
|
10310
12262
|
});
|
|
10311
|
-
await writeJson(join(stateDir, "sessions",
|
|
12263
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
10312
12264
|
active: true,
|
|
12265
|
+
mode: "ralplan",
|
|
12266
|
+
current_phase: "complete",
|
|
12267
|
+
session_id: sessionId,
|
|
12268
|
+
});
|
|
12269
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
12270
|
+
active: true,
|
|
12271
|
+
mode: "ultragoal",
|
|
10313
12272
|
current_phase: "planning",
|
|
12273
|
+
session_id: sessionId,
|
|
10314
12274
|
});
|
|
10315
|
-
await dispatchCodexNativeHook({
|
|
10316
|
-
hook_event_name: "
|
|
10317
|
-
cwd,
|
|
10318
|
-
session_id: "sess-stop-skill-repeat",
|
|
10319
|
-
thread_id: "thread-stop-skill-repeat",
|
|
10320
|
-
turn_id: "turn-stop-skill-repeat-1",
|
|
10321
|
-
}, { cwd });
|
|
10322
|
-
const repeated = await dispatchCodexNativeHook({
|
|
10323
|
-
hook_event_name: "Stop",
|
|
12275
|
+
const result = await dispatchCodexNativeHook({
|
|
12276
|
+
hook_event_name: "PreToolUse",
|
|
10324
12277
|
cwd,
|
|
10325
|
-
session_id:
|
|
10326
|
-
thread_id: "thread-
|
|
10327
|
-
|
|
10328
|
-
|
|
12278
|
+
session_id: sessionId,
|
|
12279
|
+
thread_id: "thread-ralplan-pretool-handoff",
|
|
12280
|
+
tool_name: "Edit",
|
|
12281
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
10329
12282
|
}, { cwd });
|
|
10330
|
-
assert.equal(
|
|
10331
|
-
assert.equal(
|
|
10332
|
-
assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
10333
|
-
assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
10334
|
-
assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
12283
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
12284
|
+
assert.equal(result.outputJson, null);
|
|
10335
12285
|
}
|
|
10336
12286
|
finally {
|
|
10337
12287
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -10866,6 +12816,91 @@ describe("codex native hook triage integration", () => {
|
|
|
10866
12816
|
await rm(cwd, { recursive: true, force: true });
|
|
10867
12817
|
}
|
|
10868
12818
|
});
|
|
12819
|
+
it("omits Team handoff guidance from autopilot prompt context when Team mode is disabled", async () => {
|
|
12820
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-observable-no-team-"));
|
|
12821
|
+
try {
|
|
12822
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
12823
|
+
await writeJson(join(cwd, ".omx", "setup-scope.json"), {
|
|
12824
|
+
scope: "project",
|
|
12825
|
+
teamMode: "disabled",
|
|
12826
|
+
});
|
|
12827
|
+
await writeSessionStart(cwd, "sess-autopilot-observable-no-team");
|
|
12828
|
+
const result = await dispatchCodexNativeHook({
|
|
12829
|
+
hook_event_name: "UserPromptSubmit",
|
|
12830
|
+
cwd,
|
|
12831
|
+
session_id: "sess-autopilot-observable-no-team",
|
|
12832
|
+
thread_id: "thread-autopilot-observable-no-team",
|
|
12833
|
+
turn_id: "turn-autopilot-observable-no-team",
|
|
12834
|
+
prompt: "$autopilot implement issue #2430",
|
|
12835
|
+
}, { cwd });
|
|
12836
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
12837
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
12838
|
+
assert.match(additionalContext, /detected workflow keyword "\$autopilot" -> autopilot/);
|
|
12839
|
+
assert.match(additionalContext, /\$deep-interview -> \$ralplan -> \$ultragoal -> \$code-review -> \$ultraqa/);
|
|
12840
|
+
assert.doesNotMatch(additionalContext, /\$team/);
|
|
12841
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
|
|
12842
|
+
}
|
|
12843
|
+
finally {
|
|
12844
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12845
|
+
}
|
|
12846
|
+
});
|
|
12847
|
+
it("ignores disabled $team before outside-tmux Team blocking so later workflows can activate", async () => {
|
|
12848
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-disabled-team-primary-"));
|
|
12849
|
+
try {
|
|
12850
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
12851
|
+
await writeJson(join(cwd, ".omx", "setup-scope.json"), {
|
|
12852
|
+
scope: "project",
|
|
12853
|
+
teamMode: "disabled",
|
|
12854
|
+
});
|
|
12855
|
+
await writeSessionStart(cwd, "sess-disabled-team-primary");
|
|
12856
|
+
const result = await dispatchCodexNativeHook({
|
|
12857
|
+
hook_event_name: "UserPromptSubmit",
|
|
12858
|
+
cwd,
|
|
12859
|
+
session_id: "sess-disabled-team-primary",
|
|
12860
|
+
thread_id: "thread-disabled-team-primary",
|
|
12861
|
+
turn_id: "turn-disabled-team-primary",
|
|
12862
|
+
prompt: "$team $ralph fix this",
|
|
12863
|
+
}, { cwd });
|
|
12864
|
+
assert.equal(result.skillState?.skill, "ralph");
|
|
12865
|
+
assert.equal(result.skillState?.transition_error, undefined);
|
|
12866
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
|
|
12867
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-disabled-team-primary", "ralph-state.json")), true);
|
|
12868
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
12869
|
+
assert.match(additionalContext, /detected workflow keyword "\$ralph" -> ralph/);
|
|
12870
|
+
assert.doesNotMatch(additionalContext, /Codex App\/native outside-tmux sessions cannot activate/);
|
|
12871
|
+
}
|
|
12872
|
+
finally {
|
|
12873
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12874
|
+
}
|
|
12875
|
+
});
|
|
12876
|
+
it("makes bare autopilot command activation observable in state and prompt guidance", async () => {
|
|
12877
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-bare-observable-"));
|
|
12878
|
+
try {
|
|
12879
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
12880
|
+
await writeSessionStart(cwd, "sess-autopilot-bare-observable");
|
|
12881
|
+
const result = await dispatchCodexNativeHook({
|
|
12882
|
+
hook_event_name: "UserPromptSubmit",
|
|
12883
|
+
cwd,
|
|
12884
|
+
session_id: "sess-autopilot-bare-observable",
|
|
12885
|
+
thread_id: "thread-autopilot-bare-observable",
|
|
12886
|
+
turn_id: "turn-autopilot-bare-observable",
|
|
12887
|
+
prompt: "run autopilot",
|
|
12888
|
+
}, { cwd });
|
|
12889
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
12890
|
+
assert.equal(result.skillState?.phase, "deep-interview");
|
|
12891
|
+
assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-bare-observable/autopilot-state.json");
|
|
12892
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
12893
|
+
assert.match(additionalContext, /detected workflow keyword "autopilot" -> autopilot/);
|
|
12894
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
12895
|
+
const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-bare-observable", "autopilot-state.json");
|
|
12896
|
+
const modeState = JSON.parse(await readFile(statePath, "utf-8"));
|
|
12897
|
+
assert.equal(modeState.active, true);
|
|
12898
|
+
assert.equal(modeState.current_phase, "deep-interview");
|
|
12899
|
+
}
|
|
12900
|
+
finally {
|
|
12901
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12902
|
+
}
|
|
12903
|
+
});
|
|
10869
12904
|
// ── Group 2: HEAVY injection ─────────────────────────────────────────────
|
|
10870
12905
|
it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
|
|
10871
12906
|
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));
|