oh-my-codex 0.17.3 → 0.18.1
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 +13 -5
- package/Cargo.toml +2 -1
- package/README.md +44 -19
- package/crates/omx-api/Cargo.toml +19 -0
- package/crates/omx-api/src/lib.rs +2997 -0
- package/crates/omx-api/src/main.rs +10 -0
- package/crates/omx-api/tests/cli.rs +558 -0
- package/crates/omx-explore/src/main.rs +4 -0
- package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
- package/crates/omx-sparkshell/src/exec.rs +127 -1
- package/crates/omx-sparkshell/src/main.rs +829 -30
- package/crates/omx-sparkshell/src/prompt.rs +25 -3
- package/crates/omx-sparkshell/src/redaction.rs +241 -0
- package/crates/omx-sparkshell/tests/execution.rs +702 -237
- package/dist/cli/__tests__/api.test.d.ts +2 -0
- package/dist/cli/__tests__/api.test.d.ts.map +1 -0
- package/dist/cli/__tests__/api.test.js +175 -0
- package/dist/cli/__tests__/api.test.js.map +1 -0
- package/dist/cli/__tests__/ask.test.js +72 -5
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +23 -0
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +171 -5
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
- package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
- package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
- package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
- package/dist/cli/__tests__/launch-fallback.test.js +191 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +27 -41
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +232 -35
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +25 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
- package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +227 -4
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +72 -1
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
- package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts +26 -0
- package/dist/cli/api.d.ts.map +1 -0
- package/dist/cli/api.js +153 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/codex-feature-probe.d.ts +5 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +13 -7
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +119 -10
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts +2 -0
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +43 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +12 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +460 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +2 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +1 -0
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +2 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +15 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +71 -11
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts +7 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +31 -4
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +184 -10
- package/dist/cli/ultragoal.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 +14 -3
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +3 -0
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
- package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +19 -8
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
- package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
- package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
- package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
- package/dist/config/codex-feature-flags.d.ts +4 -0
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js +4 -0
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.js +6 -6
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.d.ts +1 -0
- package/dist/config/commit-lore-guard.d.ts.map +1 -1
- package/dist/config/commit-lore-guard.js +29 -3
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +3 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +114 -10
- package/dist/config/generator.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +15 -3
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +29 -14
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +8 -3
- 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 +3 -2
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +4 -4
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
- package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
- package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
- package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
- package/dist/hud/__tests__/tmux.test.js +23 -18
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/index.d.ts +1 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +10 -4
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +9 -8
- package/dist/hud/tmux.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +75 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +3 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +71 -2
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
- package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
- package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
- package/dist/notifications/__tests__/verbosity.test.js +20 -0
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/config.d.ts.map +1 -1
- package/dist/notifications/config.js +6 -3
- package/dist/notifications/config.js.map +1 -1
- package/dist/notifications/http-client.d.ts.map +1 -1
- package/dist/notifications/http-client.js +78 -27
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/types.d.ts +2 -0
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
- package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
- package/dist/openclaw/dispatcher.d.ts +7 -4
- package/dist/openclaw/dispatcher.d.ts.map +1 -1
- package/dist/openclaw/dispatcher.js +32 -69
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +50 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/index.d.ts +8 -2
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +5 -2
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +5 -4
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +56 -15
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/code-review.d.ts +2 -2
- package/dist/pipeline/stages/code-review.d.ts.map +1 -1
- package/dist/pipeline/stages/code-review.js +5 -3
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/deep-interview.d.ts +15 -0
- package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
- package/dist/pipeline/stages/deep-interview.js +32 -0
- package/dist/pipeline/stages/deep-interview.js.map +1 -0
- package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
- package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js +2 -2
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.d.ts +19 -0
- package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
- package/dist/pipeline/stages/ultragoal.js +38 -0
- package/dist/pipeline/stages/ultragoal.js.map +1 -0
- package/dist/pipeline/stages/ultraqa.d.ts +30 -0
- package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
- package/dist/pipeline/stages/ultraqa.js +46 -0
- package/dist/pipeline/stages/ultraqa.js.map +1 -0
- package/dist/pipeline/types.d.ts +8 -6
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/pipeline/types.js +2 -2
- package/dist/scripts/__tests__/codex-native-hook.test.js +1488 -117
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +27 -2
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
- package/dist/scripts/build-api.d.ts +2 -0
- package/dist/scripts/build-api.d.ts.map +1 -0
- package/dist/scripts/build-api.js +44 -0
- package/dist/scripts/build-api.js.map +1 -0
- package/dist/scripts/cleanup-explore-harness.js +1 -0
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +364 -16
- 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 +98 -25
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-dispatcher.js +88 -0
- package/dist/scripts/notify-dispatcher.js.map +1 -1
- package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
- package/dist/scripts/notify-hook/process-runner.js +39 -17
- package/dist/scripts/notify-hook/process-runner.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +36 -14
- package/dist/scripts/notify-hook/team-dispatch.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 +26 -11
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +2 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +45 -1
- package/dist/scripts/notify-hook/team-tmux-guard.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 +27 -14
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/run-provider-advisor.js +9 -3
- package/dist/scripts/run-provider-advisor.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +4 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +101 -1
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +2 -2
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-agents.js +2 -2
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
- package/dist/sidecar/index.d.ts +1 -1
- package/dist/sidecar/index.d.ts.map +1 -1
- package/dist/sidecar/index.js +29 -12
- package/dist/sidecar/index.js.map +1 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +11 -0
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +2 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +207 -22
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +73 -28
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
- package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
- package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
- package/dist/ultragoal/artifacts.d.ts +97 -2
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +811 -256
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
- package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
- package/dist/utils/__tests__/sleep-resource.test.js +39 -0
- package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +17 -6
- package/dist/utils/sleep.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/package.json +5 -3
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
- package/plugins/oh-my-codex/hooks/hooks.json +77 -0
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -8
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
- package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -5
- package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
- package/prompts/planner.md +1 -1
- package/prompts/researcher.md +15 -10
- package/skills/autopilot/SKILL.md +77 -47
- package/skills/best-practice-research/SKILL.md +83 -0
- package/skills/cancel/SKILL.md +2 -2
- package/skills/deep-interview/SKILL.md +9 -8
- package/skills/omx-setup/SKILL.md +1 -1
- package/skills/pipeline/SKILL.md +22 -11
- package/skills/plan/SKILL.md +8 -8
- package/skills/ralph/SKILL.md +7 -0
- package/skills/ralplan/SKILL.md +5 -5
- package/skills/team/SKILL.md +1 -1
- package/skills/ultragoal/SKILL.md +38 -4
- package/skills/ultrawork/SKILL.md +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +1758 -166
- package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
- package/src/scripts/__tests__/smoke-packed-install.test.ts +39 -2
- package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
- package/src/scripts/build-api.ts +48 -0
- package/src/scripts/cleanup-explore-harness.ts +1 -0
- package/src/scripts/codex-native-hook.ts +416 -18
- package/src/scripts/codex-native-pre-post.ts +119 -25
- package/src/scripts/notify-dispatcher.ts +97 -0
- package/src/scripts/notify-hook/process-runner.ts +40 -16
- package/src/scripts/notify-hook/team-dispatch.ts +36 -13
- package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
- package/src/scripts/notify-hook/team-tmux-guard.ts +49 -0
- package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
- package/src/scripts/run-provider-advisor.ts +11 -3
- package/src/scripts/smoke-packed-install.ts +107 -0
- package/src/scripts/sync-plugin-mirror.ts +3 -3
- package/src/scripts/verify-native-agents.ts +2 -2
- package/templates/catalog-manifest.json +7 -0
|
@@ -28,6 +28,7 @@ import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
|
|
|
28
28
|
import { readAllState } from "../../hud/state.js";
|
|
29
29
|
import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
|
|
30
30
|
import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
|
|
31
|
+
import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
|
|
31
32
|
|
|
32
33
|
function nativeHookScriptPath(): string {
|
|
33
34
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
@@ -62,7 +63,44 @@ async function writeJson(path: string, value: unknown): Promise<void> {
|
|
|
62
63
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function
|
|
66
|
+
async function withLoreGuardConfig<T>(
|
|
67
|
+
value: string,
|
|
68
|
+
prefix: string,
|
|
69
|
+
run: (cwd: string) => Promise<T>,
|
|
70
|
+
): Promise<T> {
|
|
71
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-pretool-git-commit-lore-${prefix}-`));
|
|
72
|
+
const codexHome = await mkdtemp(join(tmpdir(), `omx-native-hook-codex-home-lore-${prefix}-`));
|
|
73
|
+
const defaultHome = await mkdtemp(join(tmpdir(), `omx-native-hook-home-lore-${prefix}-`));
|
|
74
|
+
const originalGuard = process.env.OMX_LORE_COMMIT_GUARD;
|
|
75
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
76
|
+
const originalHome = process.env.HOME;
|
|
77
|
+
try {
|
|
78
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
79
|
+
process.env.CODEX_HOME = codexHome;
|
|
80
|
+
process.env.HOME = defaultHome;
|
|
81
|
+
await writeFile(
|
|
82
|
+
join(codexHome, "config.toml"),
|
|
83
|
+
`[shell_environment_policy.set]\nOMX_LORE_COMMIT_GUARD = "${value}"\n`,
|
|
84
|
+
"utf-8",
|
|
85
|
+
);
|
|
86
|
+
return await run(cwd);
|
|
87
|
+
} finally {
|
|
88
|
+
if (originalGuard === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
89
|
+
else process.env.OMX_LORE_COMMIT_GUARD = originalGuard;
|
|
90
|
+
if (originalCodexHome === undefined) delete process.env.CODEX_HOME;
|
|
91
|
+
else process.env.CODEX_HOME = originalCodexHome;
|
|
92
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
93
|
+
else process.env.HOME = originalHome;
|
|
94
|
+
await rm(cwd, { recursive: true, force: true });
|
|
95
|
+
await rm(codexHome, { recursive: true, force: true });
|
|
96
|
+
await rm(defaultHome, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildWorkerStopFakeTmux(
|
|
101
|
+
tmuxLogPath: string,
|
|
102
|
+
options: { failSend?: boolean; busyLeader?: boolean } = {},
|
|
103
|
+
): string {
|
|
66
104
|
return `#!/usr/bin/env bash
|
|
67
105
|
set -eu
|
|
68
106
|
echo "$@" >> "${tmuxLogPath}"
|
|
@@ -90,7 +128,7 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
90
128
|
exit 0
|
|
91
129
|
fi
|
|
92
130
|
if [[ "$cmd" == "capture-pane" ]]; then
|
|
93
|
-
echo "› ready"
|
|
131
|
+
${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
|
|
94
132
|
exit 0
|
|
95
133
|
fi
|
|
96
134
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
@@ -726,7 +764,16 @@ describe("codex native hook dispatch", () => {
|
|
|
726
764
|
|
|
727
765
|
it("keeps subagent SessionStart from replacing the canonical leader session", async () => {
|
|
728
766
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-"));
|
|
767
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
729
768
|
try {
|
|
769
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
770
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
771
|
+
notifications: {
|
|
772
|
+
enabled: true,
|
|
773
|
+
verbosity: "session",
|
|
774
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
775
|
+
},
|
|
776
|
+
});
|
|
730
777
|
const stateDir = join(cwd, ".omx", "state");
|
|
731
778
|
const canonicalSessionId = "omx-leader-session";
|
|
732
779
|
const leaderNativeSessionId = "codex-leader-thread";
|
|
@@ -742,6 +789,16 @@ describe("codex native hook dispatch", () => {
|
|
|
742
789
|
iteration: 1,
|
|
743
790
|
max_iterations: 5,
|
|
744
791
|
});
|
|
792
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
793
|
+
await writeFile(
|
|
794
|
+
join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
|
|
795
|
+
[
|
|
796
|
+
"import { appendFileSync } from 'node:fs';",
|
|
797
|
+
"export async function onHookEvent(event) {",
|
|
798
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event, context: event.context })}\\n`);",
|
|
799
|
+
"}",
|
|
800
|
+
].join("\n"),
|
|
801
|
+
);
|
|
745
802
|
const transcriptPath = join(cwd, "subagent-rollout.jsonl");
|
|
746
803
|
await writeFile(
|
|
747
804
|
transcriptPath,
|
|
@@ -791,6 +848,11 @@ describe("codex native hook dispatch", () => {
|
|
|
791
848
|
) as { active?: boolean; current_phase?: string };
|
|
792
849
|
assert.equal(leaderRalph.active, true);
|
|
793
850
|
assert.equal(leaderRalph.current_phase, "executing");
|
|
851
|
+
assert.equal(
|
|
852
|
+
existsSync(join(cwd, "hook-events.jsonl")),
|
|
853
|
+
false,
|
|
854
|
+
"subagent SessionStart must not independently dispatch session-start hook notifications",
|
|
855
|
+
);
|
|
794
856
|
|
|
795
857
|
const tracking = JSON.parse(
|
|
796
858
|
await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
|
|
@@ -806,7 +868,257 @@ describe("codex native hook dispatch", () => {
|
|
|
806
868
|
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
|
|
807
869
|
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
|
|
808
870
|
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
|
|
871
|
+
|
|
872
|
+
await dispatchCodexNativeHook(
|
|
873
|
+
{
|
|
874
|
+
hook_event_name: "Stop",
|
|
875
|
+
cwd,
|
|
876
|
+
session_id: childNativeSessionId,
|
|
877
|
+
thread_id: childNativeSessionId,
|
|
878
|
+
turn_id: "child-stop-turn",
|
|
879
|
+
},
|
|
880
|
+
{ cwd },
|
|
881
|
+
);
|
|
882
|
+
assert.equal(
|
|
883
|
+
existsSync(join(cwd, "hook-events.jsonl")),
|
|
884
|
+
false,
|
|
885
|
+
"subagent Stop must not independently dispatch stop hook notifications",
|
|
886
|
+
);
|
|
887
|
+
} finally {
|
|
888
|
+
if (originalCodexHome === undefined) {
|
|
889
|
+
delete process.env.CODEX_HOME;
|
|
890
|
+
} else {
|
|
891
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
892
|
+
}
|
|
893
|
+
await rm(cwd, { recursive: true, force: true });
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it("suppresses child-agent SessionStart hook dispatch at minimal verbosity", async () => {
|
|
898
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-minimal-"));
|
|
899
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
900
|
+
try {
|
|
901
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
902
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
903
|
+
notifications: {
|
|
904
|
+
enabled: true,
|
|
905
|
+
verbosity: "minimal",
|
|
906
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
910
|
+
const canonicalSessionId = "omx-leader-session-minimal";
|
|
911
|
+
const leaderNativeSessionId = "codex-leader-thread-minimal";
|
|
912
|
+
const childNativeSessionId = "codex-child-thread-minimal";
|
|
913
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
914
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
915
|
+
nativeSessionId: leaderNativeSessionId,
|
|
916
|
+
});
|
|
917
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
918
|
+
await writeFile(
|
|
919
|
+
join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
|
|
920
|
+
[
|
|
921
|
+
"import { appendFileSync } from 'node:fs';",
|
|
922
|
+
"export async function onHookEvent(event) {",
|
|
923
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
924
|
+
"}",
|
|
925
|
+
].join("\n"),
|
|
926
|
+
);
|
|
927
|
+
const transcriptPath = join(cwd, "minimal-subagent-rollout.jsonl");
|
|
928
|
+
await writeFile(
|
|
929
|
+
transcriptPath,
|
|
930
|
+
`${JSON.stringify({
|
|
931
|
+
type: "session_meta",
|
|
932
|
+
payload: {
|
|
933
|
+
id: childNativeSessionId,
|
|
934
|
+
source: {
|
|
935
|
+
subagent: {
|
|
936
|
+
thread_spawn: {
|
|
937
|
+
parent_thread_id: leaderNativeSessionId,
|
|
938
|
+
agent_role: "verifier",
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
})}\n`,
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
await dispatchCodexNativeHook(
|
|
947
|
+
{
|
|
948
|
+
hook_event_name: "SessionStart",
|
|
949
|
+
cwd,
|
|
950
|
+
session_id: childNativeSessionId,
|
|
951
|
+
transcript_path: transcriptPath,
|
|
952
|
+
},
|
|
953
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
assert.equal(
|
|
957
|
+
existsSync(join(cwd, "hook-events.jsonl")),
|
|
958
|
+
false,
|
|
959
|
+
"subagent SessionStart must be suppressed at minimal verbosity",
|
|
960
|
+
);
|
|
961
|
+
} finally {
|
|
962
|
+
if (originalCodexHome === undefined) {
|
|
963
|
+
delete process.env.CODEX_HOME;
|
|
964
|
+
} else {
|
|
965
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
966
|
+
}
|
|
967
|
+
await rm(cwd, { recursive: true, force: true });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it("allows explicit child-agent lifecycle hook dispatch when includeChildAgents is enabled", async () => {
|
|
972
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-include-"));
|
|
973
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
974
|
+
try {
|
|
975
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
976
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
977
|
+
notifications: {
|
|
978
|
+
enabled: true,
|
|
979
|
+
verbosity: "session",
|
|
980
|
+
includeChildAgents: true,
|
|
981
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
985
|
+
const canonicalSessionId = "omx-leader-session-include";
|
|
986
|
+
const leaderNativeSessionId = "codex-leader-thread-include";
|
|
987
|
+
const childNativeSessionId = "codex-child-thread-include";
|
|
988
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
989
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
990
|
+
nativeSessionId: leaderNativeSessionId,
|
|
991
|
+
});
|
|
992
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
993
|
+
await writeFile(
|
|
994
|
+
join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
|
|
995
|
+
[
|
|
996
|
+
"import { appendFileSync } from 'node:fs';",
|
|
997
|
+
"export async function onHookEvent(event) {",
|
|
998
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
999
|
+
"}",
|
|
1000
|
+
].join("\n"),
|
|
1001
|
+
);
|
|
1002
|
+
const transcriptPath = join(cwd, "included-subagent-rollout.jsonl");
|
|
1003
|
+
await writeFile(
|
|
1004
|
+
transcriptPath,
|
|
1005
|
+
`${JSON.stringify({
|
|
1006
|
+
type: "session_meta",
|
|
1007
|
+
payload: {
|
|
1008
|
+
id: childNativeSessionId,
|
|
1009
|
+
source: {
|
|
1010
|
+
subagent: {
|
|
1011
|
+
thread_spawn: {
|
|
1012
|
+
parent_thread_id: leaderNativeSessionId,
|
|
1013
|
+
agent_role: "verifier",
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
})}\n`,
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
await dispatchCodexNativeHook(
|
|
1022
|
+
{
|
|
1023
|
+
hook_event_name: "SessionStart",
|
|
1024
|
+
cwd,
|
|
1025
|
+
session_id: childNativeSessionId,
|
|
1026
|
+
transcript_path: transcriptPath,
|
|
1027
|
+
},
|
|
1028
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
await dispatchCodexNativeHook(
|
|
1032
|
+
{
|
|
1033
|
+
hook_event_name: "Stop",
|
|
1034
|
+
cwd,
|
|
1035
|
+
session_id: childNativeSessionId,
|
|
1036
|
+
thread_id: childNativeSessionId,
|
|
1037
|
+
turn_id: "included-child-stop-turn",
|
|
1038
|
+
},
|
|
1039
|
+
{ cwd },
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
|
|
1043
|
+
assert.match(hookEvents, /"event":"session-start"/);
|
|
1044
|
+
assert.match(hookEvents, /"event":"stop"/);
|
|
1045
|
+
} finally {
|
|
1046
|
+
if (originalCodexHome === undefined) {
|
|
1047
|
+
delete process.env.CODEX_HOME;
|
|
1048
|
+
} else {
|
|
1049
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
1050
|
+
}
|
|
1051
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it("allows child-agent lifecycle hook dispatch at agent verbosity", async () => {
|
|
1056
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-agent-"));
|
|
1057
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
1058
|
+
try {
|
|
1059
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
1060
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
1061
|
+
notifications: {
|
|
1062
|
+
enabled: true,
|
|
1063
|
+
verbosity: "agent",
|
|
1064
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1068
|
+
const canonicalSessionId = "omx-leader-session-agent";
|
|
1069
|
+
const leaderNativeSessionId = "codex-leader-thread-agent";
|
|
1070
|
+
const childNativeSessionId = "codex-child-thread-agent";
|
|
1071
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1072
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1073
|
+
nativeSessionId: leaderNativeSessionId,
|
|
1074
|
+
});
|
|
1075
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
1076
|
+
await writeFile(
|
|
1077
|
+
join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
|
|
1078
|
+
[
|
|
1079
|
+
"import { appendFileSync } from 'node:fs';",
|
|
1080
|
+
"export async function onHookEvent(event) {",
|
|
1081
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
1082
|
+
"}",
|
|
1083
|
+
].join("\n"),
|
|
1084
|
+
);
|
|
1085
|
+
const transcriptPath = join(cwd, "agent-verbosity-subagent-rollout.jsonl");
|
|
1086
|
+
await writeFile(
|
|
1087
|
+
transcriptPath,
|
|
1088
|
+
`${JSON.stringify({
|
|
1089
|
+
type: "session_meta",
|
|
1090
|
+
payload: {
|
|
1091
|
+
id: childNativeSessionId,
|
|
1092
|
+
source: {
|
|
1093
|
+
subagent: {
|
|
1094
|
+
thread_spawn: {
|
|
1095
|
+
parent_thread_id: leaderNativeSessionId,
|
|
1096
|
+
agent_role: "verifier",
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
})}\n`,
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
await dispatchCodexNativeHook(
|
|
1105
|
+
{
|
|
1106
|
+
hook_event_name: "SessionStart",
|
|
1107
|
+
cwd,
|
|
1108
|
+
session_id: childNativeSessionId,
|
|
1109
|
+
transcript_path: transcriptPath,
|
|
1110
|
+
},
|
|
1111
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
|
|
1115
|
+
assert.match(hookEvents, /"event":"session-start"/);
|
|
809
1116
|
} finally {
|
|
1117
|
+
if (originalCodexHome === undefined) {
|
|
1118
|
+
delete process.env.CODEX_HOME;
|
|
1119
|
+
} else {
|
|
1120
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
1121
|
+
}
|
|
810
1122
|
await rm(cwd, { recursive: true, force: true });
|
|
811
1123
|
}
|
|
812
1124
|
});
|
|
@@ -1495,6 +1807,66 @@ describe("codex native hook dispatch", () => {
|
|
|
1495
1807
|
}
|
|
1496
1808
|
});
|
|
1497
1809
|
|
|
1810
|
+
it("does not repeat performance-goal reconciliation after a recorded objective mismatch blocker", async () => {
|
|
1811
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-mismatch-blocked-stop-"));
|
|
1812
|
+
try {
|
|
1813
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
1814
|
+
version: 1,
|
|
1815
|
+
workflow: "performance-goal",
|
|
1816
|
+
slug: "latency",
|
|
1817
|
+
objective: "Reduce latency",
|
|
1818
|
+
status: "blocked",
|
|
1819
|
+
lastValidation: {
|
|
1820
|
+
status: "blocked",
|
|
1821
|
+
evidence: "omx performance-goal complete rejected the fresh get_goal snapshot: Codex goal objective mismatch: expected \"reduce latency\", got \"legacy objective\".",
|
|
1822
|
+
recordedAt: "2026-05-20T00:00:00.000Z",
|
|
1823
|
+
},
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
const result = await dispatchCodexNativeHook({
|
|
1827
|
+
hook_event_name: "Stop",
|
|
1828
|
+
cwd,
|
|
1829
|
+
session_id: "sess-performance-mismatch-blocked-stop",
|
|
1830
|
+
thread_id: "thread-performance-mismatch-blocked-stop",
|
|
1831
|
+
last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
|
|
1832
|
+
}, { cwd });
|
|
1833
|
+
|
|
1834
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
1835
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
|
|
1836
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1837
|
+
} finally {
|
|
1838
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
it("does not block Stop for an already complete performance-goal state", async () => {
|
|
1843
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-complete-stop-"));
|
|
1844
|
+
try {
|
|
1845
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
1846
|
+
version: 1,
|
|
1847
|
+
workflow: "performance-goal",
|
|
1848
|
+
slug: "latency",
|
|
1849
|
+
objective: "Reduce latency",
|
|
1850
|
+
status: "complete",
|
|
1851
|
+
completedAt: "2026-05-20T00:00:00.000Z",
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
const result = await dispatchCodexNativeHook({
|
|
1855
|
+
hook_event_name: "Stop",
|
|
1856
|
+
cwd,
|
|
1857
|
+
session_id: "sess-performance-complete-stop",
|
|
1858
|
+
thread_id: "thread-performance-complete-stop",
|
|
1859
|
+
last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
|
|
1860
|
+
}, { cwd });
|
|
1861
|
+
|
|
1862
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
1863
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
|
|
1864
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1865
|
+
} finally {
|
|
1866
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1498
1870
|
it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
|
|
1499
1871
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
|
|
1500
1872
|
try {
|
|
@@ -1543,7 +1915,7 @@ describe("codex native hook dispatch", () => {
|
|
|
1543
1915
|
}
|
|
1544
1916
|
});
|
|
1545
1917
|
|
|
1546
|
-
it("blocks ultragoal Stop with blocked checkpoint and
|
|
1918
|
+
it("blocks ultragoal Stop with blocked checkpoint and available-goal-context remediation for completed legacy snapshots", async () => {
|
|
1547
1919
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
|
|
1548
1920
|
try {
|
|
1549
1921
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
@@ -1564,7 +1936,8 @@ describe("codex native hook dispatch", () => {
|
|
|
1564
1936
|
assert.equal(result.outputJson?.decision, "block");
|
|
1565
1937
|
assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
1566
1938
|
assert.match(output, /--status blocked/);
|
|
1567
|
-
assert.match(output, /
|
|
1939
|
+
assert.match(output, /Codex goal context/);
|
|
1940
|
+
assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
|
|
1568
1941
|
assert.match(output, /Hooks must not mutate Codex goal state/);
|
|
1569
1942
|
} finally {
|
|
1570
1943
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -1578,7 +1951,7 @@ describe("codex native hook dispatch", () => {
|
|
|
1578
1951
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
1579
1952
|
version: 1,
|
|
1580
1953
|
codexGoalMode: "aggregate",
|
|
1581
|
-
codexObjective: "Complete
|
|
1954
|
+
codexObjective: "Complete the durable ultragoal plan in .omx/ultragoal/goals.json, including later accepted/appended stories, under the original brief constraints; use .omx/ultragoal/ledger.jsonl as the audit trail.",
|
|
1582
1955
|
activeGoalId: "G001-micro",
|
|
1583
1956
|
aggregateCompletion: {
|
|
1584
1957
|
status: "complete",
|
|
@@ -1701,30 +2074,95 @@ describe("codex native hook dispatch", () => {
|
|
|
1701
2074
|
}
|
|
1702
2075
|
});
|
|
1703
2076
|
|
|
1704
|
-
it("
|
|
1705
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
2077
|
+
it("does not repeat Stop block when the last autoresearch-goal completion attempt reported objective mismatch", async () => {
|
|
2078
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-reported-stop-"));
|
|
1706
2079
|
try {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
await writeJson(join(stateDir, "session.json"), {
|
|
1714
|
-
session_id: canonicalSessionId,
|
|
1715
|
-
native_session_id: leaderNativeSessionId,
|
|
2080
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "mission.json"), {
|
|
2081
|
+
version: 1,
|
|
2082
|
+
workflow: "autoresearch-goal",
|
|
2083
|
+
slug: "mismatched-mission",
|
|
2084
|
+
topic: "Passing research bound to another Codex goal",
|
|
2085
|
+
status: "passed",
|
|
1716
2086
|
});
|
|
1717
|
-
await writeJson(join(
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
2087
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "completion.json"), {
|
|
2088
|
+
verdict: "pass",
|
|
2089
|
+
passed: true,
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
const result = await dispatchCodexNativeHook({
|
|
2093
|
+
hook_event_name: "Stop",
|
|
2094
|
+
cwd,
|
|
2095
|
+
session_id: "sess-autoresearch-mismatch-reported-stop",
|
|
2096
|
+
thread_id: "thread-autoresearch-mismatch-reported-stop",
|
|
2097
|
+
last_assistant_message: [
|
|
2098
|
+
"I called get_goal and ran omx autoresearch-goal complete --slug mismatched-mission --codex-goal-json /tmp/snapshot.json.",
|
|
2099
|
+
"The autoresearch-goal completion failed with Codex goal objective mismatch, so I will not repeat the same complete command blindly in this thread.",
|
|
2100
|
+
].join("\n"),
|
|
2101
|
+
}, { cwd });
|
|
2102
|
+
|
|
2103
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
2104
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /autoresearch-goal complete --slug mismatched-mission/);
|
|
2105
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
2106
|
+
} finally {
|
|
2107
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
|
|
2111
|
+
it("still blocks later autoresearch-goal completion claims after an objective mismatch if no mismatch is reported in the final answer", async () => {
|
|
2112
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-later-retry-stop-"));
|
|
2113
|
+
try {
|
|
2114
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "mission.json"), {
|
|
2115
|
+
version: 1,
|
|
2116
|
+
workflow: "autoresearch-goal",
|
|
2117
|
+
slug: "retryable-mission",
|
|
2118
|
+
topic: "Passing research that can still retry with the correct snapshot",
|
|
2119
|
+
status: "passed",
|
|
2120
|
+
});
|
|
2121
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "completion.json"), {
|
|
2122
|
+
verdict: "pass",
|
|
2123
|
+
passed: true,
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
const result = await dispatchCodexNativeHook({
|
|
2127
|
+
hook_event_name: "Stop",
|
|
2128
|
+
cwd,
|
|
2129
|
+
session_id: "sess-autoresearch-mismatch-later-retry-stop",
|
|
2130
|
+
thread_id: "thread-autoresearch-mismatch-later-retry-stop",
|
|
2131
|
+
last_assistant_message: "Autoresearch goal complete; next call update_goal({status: \"complete\"}).",
|
|
2132
|
+
}, { cwd });
|
|
2133
|
+
|
|
2134
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2135
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
2136
|
+
assert.match(JSON.stringify(result.outputJson), /omx autoresearch-goal complete --slug retryable-mission/);
|
|
2137
|
+
} finally {
|
|
2138
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
|
|
2143
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
|
|
2144
|
+
try {
|
|
2145
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2146
|
+
const canonicalSessionId = "sess-parent";
|
|
2147
|
+
const leaderNativeSessionId = "native-parent-thread";
|
|
2148
|
+
const childNativeSessionId = "native-child-thread";
|
|
2149
|
+
const nowIso = new Date().toISOString();
|
|
2150
|
+
|
|
2151
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2152
|
+
session_id: canonicalSessionId,
|
|
2153
|
+
native_session_id: leaderNativeSessionId,
|
|
2154
|
+
});
|
|
2155
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2156
|
+
schemaVersion: 1,
|
|
2157
|
+
sessions: {
|
|
2158
|
+
[canonicalSessionId]: {
|
|
2159
|
+
session_id: canonicalSessionId,
|
|
2160
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2161
|
+
updated_at: nowIso,
|
|
2162
|
+
threads: {
|
|
2163
|
+
[leaderNativeSessionId]: {
|
|
2164
|
+
thread_id: leaderNativeSessionId,
|
|
2165
|
+
kind: "leader",
|
|
1728
2166
|
first_seen_at: nowIso,
|
|
1729
2167
|
last_seen_at: nowIso,
|
|
1730
2168
|
turn_count: 1,
|
|
@@ -1822,12 +2260,190 @@ describe("codex native hook dispatch", () => {
|
|
|
1822
2260
|
assert.match(message, /get_goal/);
|
|
1823
2261
|
assert.match(message, /create_goal/);
|
|
1824
2262
|
assert.match(message, /update_goal/);
|
|
2263
|
+
assert.match(message, /does not call `\/goal clear`/);
|
|
2264
|
+
assert.match(message, /multiple sequential ultragoal runs/);
|
|
1825
2265
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
|
|
1826
2266
|
} finally {
|
|
1827
2267
|
await rm(cwd, { recursive: true, force: true });
|
|
1828
2268
|
}
|
|
1829
2269
|
});
|
|
1830
2270
|
|
|
2271
|
+
it("applies only explicit structured UserPromptSubmit ultragoal steering directives", async () => {
|
|
2272
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-"));
|
|
2273
|
+
try {
|
|
2274
|
+
await createUltragoalPlan(cwd, {
|
|
2275
|
+
brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal hook steering fixture",
|
|
2276
|
+
goals: [{ title: "First", objective: "Complete first milestone with tests." }],
|
|
2277
|
+
});
|
|
2278
|
+
|
|
2279
|
+
const prose = await dispatchCodexNativeHook(
|
|
2280
|
+
{
|
|
2281
|
+
hook_event_name: "UserPromptSubmit",
|
|
2282
|
+
cwd,
|
|
2283
|
+
session_id: "sess-ultragoal-steer-1",
|
|
2284
|
+
prompt: "Please add a subgoal for docs later; this is normal prose, not a directive.",
|
|
2285
|
+
},
|
|
2286
|
+
{ cwd },
|
|
2287
|
+
);
|
|
2288
|
+
assert.equal(prose.outputJson, null);
|
|
2289
|
+
assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
|
|
2290
|
+
|
|
2291
|
+
const jsonExample = await dispatchCodexNativeHook(
|
|
2292
|
+
{
|
|
2293
|
+
hook_event_name: "UserPromptSubmit",
|
|
2294
|
+
cwd,
|
|
2295
|
+
session_id: "sess-ultragoal-steer-1",
|
|
2296
|
+
prompt: `Here is an inert example:\n\`\`\`json\n${JSON.stringify({
|
|
2297
|
+
kind: "add_subgoal",
|
|
2298
|
+
source: "user_prompt_submit",
|
|
2299
|
+
evidence: "Example JSON should not mutate .omx/ultragoal.",
|
|
2300
|
+
rationale: "Only explicit steering fences or labels are executable.",
|
|
2301
|
+
title: "Inert JSON example",
|
|
2302
|
+
objective: "This example must not be added.",
|
|
2303
|
+
})}\n\`\`\``,
|
|
2304
|
+
},
|
|
2305
|
+
{ cwd },
|
|
2306
|
+
);
|
|
2307
|
+
assert.equal(jsonExample.outputJson, null);
|
|
2308
|
+
assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
|
|
2309
|
+
|
|
2310
|
+
const result = await dispatchCodexNativeHook(
|
|
2311
|
+
{
|
|
2312
|
+
hook_event_name: "UserPromptSubmit",
|
|
2313
|
+
cwd,
|
|
2314
|
+
session_id: "sess-ultragoal-steer-1",
|
|
2315
|
+
prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
|
|
2316
|
+
kind: "add_subgoal",
|
|
2317
|
+
source: "user_prompt_submit",
|
|
2318
|
+
evidence: "Prompt-submit supplied a structured .omx/ultragoal directive for G002-cli-and-prompt-submit-bridge.",
|
|
2319
|
+
rationale: "Add bounded hook regression work while preserving all completion gates.",
|
|
2320
|
+
title: "Prompt bridge regression",
|
|
2321
|
+
objective: "Verify UserPromptSubmit bounded steering bridge with tests.",
|
|
2322
|
+
})}`,
|
|
2323
|
+
},
|
|
2324
|
+
{ cwd },
|
|
2325
|
+
);
|
|
2326
|
+
|
|
2327
|
+
const message = String(
|
|
2328
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
2329
|
+
);
|
|
2330
|
+
assert.match(message, /bounded \.omx\/ultragoal steering/);
|
|
2331
|
+
assert.match(message, /G002-cli-and-prompt-submit-bridge/);
|
|
2332
|
+
assert.match(message, /accepted/);
|
|
2333
|
+
const plan = await readUltragoalPlan(cwd);
|
|
2334
|
+
assert.equal(plan.goals.length, 2);
|
|
2335
|
+
assert.equal(plan.goals[1]?.title, "Prompt bridge regression");
|
|
2336
|
+
} finally {
|
|
2337
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2338
|
+
}
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
it("does not apply UserPromptSubmit ultragoal steering from native subagent prompts", async () => {
|
|
2342
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-subagent-"));
|
|
2343
|
+
try {
|
|
2344
|
+
await createUltragoalPlan(cwd, {
|
|
2345
|
+
brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal subagent steering fixture",
|
|
2346
|
+
goals: [{ title: "First", objective: "Complete first milestone with tests." }],
|
|
2347
|
+
});
|
|
2348
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2349
|
+
const canonicalSessionId = "sess-ultragoal-parent";
|
|
2350
|
+
const leaderNativeSessionId = "native-ultragoal-parent";
|
|
2351
|
+
const childNativeSessionId = "native-ultragoal-child";
|
|
2352
|
+
const nowIso = new Date().toISOString();
|
|
2353
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2354
|
+
session_id: canonicalSessionId,
|
|
2355
|
+
native_session_id: leaderNativeSessionId,
|
|
2356
|
+
});
|
|
2357
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2358
|
+
schemaVersion: 1,
|
|
2359
|
+
sessions: {
|
|
2360
|
+
[canonicalSessionId]: {
|
|
2361
|
+
session_id: canonicalSessionId,
|
|
2362
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2363
|
+
updated_at: nowIso,
|
|
2364
|
+
threads: {
|
|
2365
|
+
[leaderNativeSessionId]: {
|
|
2366
|
+
thread_id: leaderNativeSessionId,
|
|
2367
|
+
kind: "leader",
|
|
2368
|
+
first_seen_at: nowIso,
|
|
2369
|
+
last_seen_at: nowIso,
|
|
2370
|
+
turn_count: 1,
|
|
2371
|
+
},
|
|
2372
|
+
[childNativeSessionId]: {
|
|
2373
|
+
thread_id: childNativeSessionId,
|
|
2374
|
+
kind: "subagent",
|
|
2375
|
+
first_seen_at: nowIso,
|
|
2376
|
+
last_seen_at: nowIso,
|
|
2377
|
+
turn_count: 1,
|
|
2378
|
+
mode: "architect",
|
|
2379
|
+
},
|
|
2380
|
+
},
|
|
2381
|
+
},
|
|
2382
|
+
},
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
const result = await dispatchCodexNativeHook(
|
|
2386
|
+
{
|
|
2387
|
+
hook_event_name: "UserPromptSubmit",
|
|
2388
|
+
cwd,
|
|
2389
|
+
session_id: childNativeSessionId,
|
|
2390
|
+
thread_id: childNativeSessionId,
|
|
2391
|
+
turn_id: "turn-ultragoal-child-1",
|
|
2392
|
+
prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
|
|
2393
|
+
kind: "add_subgoal",
|
|
2394
|
+
source: "user_prompt_submit",
|
|
2395
|
+
evidence: "Subagent prompt text must be literal delegated context.",
|
|
2396
|
+
rationale: "Subagent prompts should not mutate the parent .omx/ultragoal ledger.",
|
|
2397
|
+
title: "Subagent should not add this",
|
|
2398
|
+
objective: "This must remain literal prompt text.",
|
|
2399
|
+
})}`,
|
|
2400
|
+
},
|
|
2401
|
+
{ cwd },
|
|
2402
|
+
);
|
|
2403
|
+
|
|
2404
|
+
assert.equal(result.outputJson, null);
|
|
2405
|
+
const plan = await readUltragoalPlan(cwd);
|
|
2406
|
+
assert.equal(plan.goals.length, 1);
|
|
2407
|
+
const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
|
|
2408
|
+
assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 0);
|
|
2409
|
+
assert.equal((ledger.match(/"event":"steering_rejected"/g) ?? []).length, 0);
|
|
2410
|
+
} finally {
|
|
2411
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
|
|
2415
|
+
it("dedupes repeated UserPromptSubmit ultragoal steering directives by prompt signature", async () => {
|
|
2416
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-dedupe-"));
|
|
2417
|
+
try {
|
|
2418
|
+
await createUltragoalPlan(cwd, {
|
|
2419
|
+
brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal dedupe fixture",
|
|
2420
|
+
goals: [{ title: "First", objective: "Complete first milestone with tests." }],
|
|
2421
|
+
});
|
|
2422
|
+
const prompt = `\`\`\`omx-ultragoal-steer
|
|
2423
|
+
${JSON.stringify({
|
|
2424
|
+
kind: "add_subgoal",
|
|
2425
|
+
source: "user_prompt_submit",
|
|
2426
|
+
evidence: "Structured prompt-submit directive adds exactly one deduped goal.",
|
|
2427
|
+
rationale: "Use idempotent bridge semantics for repeated hook delivery.",
|
|
2428
|
+
title: "Deduped bridge regression",
|
|
2429
|
+
objective: "Verify repeated UserPromptSubmit steering does not duplicate goals.",
|
|
2430
|
+
})}
|
|
2431
|
+
\`\`\``;
|
|
2432
|
+
await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
|
|
2433
|
+
const second = await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
|
|
2434
|
+
const message = String(
|
|
2435
|
+
(second.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
2436
|
+
);
|
|
2437
|
+
assert.match(message, /deduped/);
|
|
2438
|
+
const plan = await readUltragoalPlan(cwd);
|
|
2439
|
+
assert.equal(plan.goals.filter((goal) => goal.title === "Deduped bridge regression").length, 1);
|
|
2440
|
+
const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
|
|
2441
|
+
assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 1);
|
|
2442
|
+
} finally {
|
|
2443
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
|
|
1831
2447
|
it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
|
|
1832
2448
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
|
|
1833
2449
|
try {
|
|
@@ -2863,6 +3479,52 @@ exit 0
|
|
|
2863
3479
|
}
|
|
2864
3480
|
});
|
|
2865
3481
|
|
|
3482
|
+
it("does not block Bash commands that only mention omx question in quoted arguments", async () => {
|
|
3483
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-quoted-mention-"));
|
|
3484
|
+
try {
|
|
3485
|
+
const result = await dispatchCodexNativeHook(
|
|
3486
|
+
{
|
|
3487
|
+
hook_event_name: "PreToolUse",
|
|
3488
|
+
cwd,
|
|
3489
|
+
tool_name: "Bash",
|
|
3490
|
+
tool_use_id: "tool-question-quoted-mention",
|
|
3491
|
+
tool_input: {
|
|
3492
|
+
command: `omx ultragoal create-goals --brief "Deep interview says omx question failed in tmux"`,
|
|
3493
|
+
},
|
|
3494
|
+
},
|
|
3495
|
+
{ cwd },
|
|
3496
|
+
);
|
|
3497
|
+
|
|
3498
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3499
|
+
assert.equal(result.outputJson, null);
|
|
3500
|
+
} finally {
|
|
3501
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3502
|
+
}
|
|
3503
|
+
});
|
|
3504
|
+
|
|
3505
|
+
it("does not block Bash heredocs that only document omx question text", async () => {
|
|
3506
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-heredoc-mention-"));
|
|
3507
|
+
try {
|
|
3508
|
+
const result = await dispatchCodexNativeHook(
|
|
3509
|
+
{
|
|
3510
|
+
hook_event_name: "PreToolUse",
|
|
3511
|
+
cwd,
|
|
3512
|
+
tool_name: "Bash",
|
|
3513
|
+
tool_use_id: "tool-question-heredoc-mention",
|
|
3514
|
+
tool_input: {
|
|
3515
|
+
command: `cat > issue-notes.md <<'EOF'\nomx question failed in the attached tmux pane\nEOF`,
|
|
3516
|
+
},
|
|
3517
|
+
},
|
|
3518
|
+
{ cwd },
|
|
3519
|
+
);
|
|
3520
|
+
|
|
3521
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3522
|
+
assert.equal(result.outputJson, null);
|
|
3523
|
+
} finally {
|
|
3524
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3525
|
+
}
|
|
3526
|
+
});
|
|
3527
|
+
|
|
2866
3528
|
it("allows Bash omx question when the command preserves the leader-pane return hint", async () => {
|
|
2867
3529
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-allow-"));
|
|
2868
3530
|
try {
|
|
@@ -3654,7 +4316,7 @@ exit 0
|
|
|
3654
4316
|
cwd,
|
|
3655
4317
|
tool_name: "Bash",
|
|
3656
4318
|
tool_use_id: "tool-slop-git-priority",
|
|
3657
|
-
tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
|
|
4319
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "quick hack fallback if it fails"' },
|
|
3658
4320
|
},
|
|
3659
4321
|
{ cwd },
|
|
3660
4322
|
);
|
|
@@ -3677,7 +4339,7 @@ exit 0
|
|
|
3677
4339
|
cwd,
|
|
3678
4340
|
tool_name: "Bash",
|
|
3679
4341
|
tool_use_id: "tool-git-commit-invalid",
|
|
3680
|
-
tool_input: { command: 'git commit -m "fix tests"' },
|
|
4342
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
|
|
3681
4343
|
},
|
|
3682
4344
|
{ cwd },
|
|
3683
4345
|
);
|
|
@@ -3706,24 +4368,26 @@ exit 0
|
|
|
3706
4368
|
}
|
|
3707
4369
|
});
|
|
3708
4370
|
|
|
3709
|
-
|
|
3710
|
-
|
|
4371
|
+
|
|
4372
|
+
it("blocks PreToolUse git commit when process env explicitly enables the Lore commit guard", async () => {
|
|
4373
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-enabled-"));
|
|
3711
4374
|
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3712
4375
|
try {
|
|
3713
|
-
process.env.OMX_LORE_COMMIT_GUARD = "
|
|
4376
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
3714
4377
|
const result = await dispatchCodexNativeHook(
|
|
3715
4378
|
{
|
|
3716
4379
|
hook_event_name: "PreToolUse",
|
|
3717
4380
|
cwd,
|
|
3718
4381
|
tool_name: "Bash",
|
|
3719
|
-
tool_use_id: "tool-git-commit-lore-
|
|
3720
|
-
tool_input: { command: 'git commit -m "fix
|
|
4382
|
+
tool_use_id: "tool-git-commit-lore-env-enabled",
|
|
4383
|
+
tool_input: { command: 'git commit -m "fix tests"' },
|
|
3721
4384
|
},
|
|
3722
4385
|
{ cwd },
|
|
3723
4386
|
);
|
|
3724
4387
|
|
|
3725
4388
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3726
|
-
assert.equal(result.outputJson
|
|
4389
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
4390
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3727
4391
|
} finally {
|
|
3728
4392
|
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3729
4393
|
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
@@ -3731,16 +4395,18 @@ exit 0
|
|
|
3731
4395
|
}
|
|
3732
4396
|
});
|
|
3733
4397
|
|
|
3734
|
-
it("allows non-Lore git commit messages when the Lore commit guard is disabled
|
|
3735
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-
|
|
4398
|
+
it("allows non-Lore git commit messages when the Lore commit guard is disabled by default", async () => {
|
|
4399
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
|
|
4400
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3736
4401
|
try {
|
|
4402
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3737
4403
|
const result = await dispatchCodexNativeHook(
|
|
3738
4404
|
{
|
|
3739
4405
|
hook_event_name: "PreToolUse",
|
|
3740
4406
|
cwd,
|
|
3741
4407
|
tool_name: "Bash",
|
|
3742
|
-
tool_use_id: "tool-git-commit-lore-
|
|
3743
|
-
tool_input: { command: '
|
|
4408
|
+
tool_use_id: "tool-git-commit-lore-disabled",
|
|
4409
|
+
tool_input: { command: 'git commit -m "fix: use conventional commit"' },
|
|
3744
4410
|
},
|
|
3745
4411
|
{ cwd },
|
|
3746
4412
|
);
|
|
@@ -3748,20 +4414,21 @@ exit 0
|
|
|
3748
4414
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3749
4415
|
assert.equal(result.outputJson, null);
|
|
3750
4416
|
} finally {
|
|
4417
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
4418
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3751
4419
|
await rm(cwd, { recursive: true, force: true });
|
|
3752
4420
|
}
|
|
3753
4421
|
});
|
|
3754
4422
|
|
|
3755
|
-
it("
|
|
3756
|
-
|
|
3757
|
-
try {
|
|
4423
|
+
it("blocks non-Lore git commit messages when the Lore commit guard is enabled in CODEX_HOME config.toml", async () => {
|
|
4424
|
+
await withLoreGuardConfig("1", "config-enabled", async (cwd) => {
|
|
3758
4425
|
const result = await dispatchCodexNativeHook(
|
|
3759
4426
|
{
|
|
3760
4427
|
hook_event_name: "PreToolUse",
|
|
3761
4428
|
cwd,
|
|
3762
4429
|
tool_name: "Bash",
|
|
3763
|
-
tool_use_id: "tool-git-commit-lore-
|
|
3764
|
-
tool_input: { command: '
|
|
4430
|
+
tool_use_id: "tool-git-commit-lore-config-enabled",
|
|
4431
|
+
tool_input: { command: 'git commit -m "fix: conventional"' },
|
|
3765
4432
|
},
|
|
3766
4433
|
{ cwd },
|
|
3767
4434
|
);
|
|
@@ -3769,49 +4436,36 @@ exit 0
|
|
|
3769
4436
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3770
4437
|
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3771
4438
|
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3772
|
-
}
|
|
3773
|
-
await rm(cwd, { recursive: true, force: true });
|
|
3774
|
-
}
|
|
4439
|
+
});
|
|
3775
4440
|
});
|
|
3776
4441
|
|
|
3777
|
-
it("
|
|
3778
|
-
|
|
3779
|
-
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3780
|
-
try {
|
|
3781
|
-
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
4442
|
+
it("allows non-Lore git commit messages when the Lore commit guard is disabled in CODEX_HOME config.toml", async () => {
|
|
4443
|
+
await withLoreGuardConfig("0", "config-disabled", async (cwd) => {
|
|
3782
4444
|
const result = await dispatchCodexNativeHook(
|
|
3783
4445
|
{
|
|
3784
4446
|
hook_event_name: "PreToolUse",
|
|
3785
4447
|
cwd,
|
|
3786
4448
|
tool_name: "Bash",
|
|
3787
|
-
tool_use_id: "tool-git-commit-lore-
|
|
3788
|
-
tool_input: { command: '
|
|
4449
|
+
tool_use_id: "tool-git-commit-lore-config-disabled",
|
|
4450
|
+
tool_input: { command: 'git commit -m "fix: use conventional commit"' },
|
|
3789
4451
|
},
|
|
3790
4452
|
{ cwd },
|
|
3791
4453
|
);
|
|
3792
4454
|
|
|
3793
4455
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3794
|
-
assert.equal(
|
|
3795
|
-
|
|
3796
|
-
} finally {
|
|
3797
|
-
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3798
|
-
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3799
|
-
await rm(cwd, { recursive: true, force: true });
|
|
3800
|
-
}
|
|
4456
|
+
assert.equal(result.outputJson, null);
|
|
4457
|
+
});
|
|
3801
4458
|
});
|
|
3802
4459
|
|
|
3803
|
-
it("
|
|
3804
|
-
|
|
3805
|
-
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3806
|
-
try {
|
|
3807
|
-
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
4460
|
+
it("lets inline Lore commit guard values override a disabled CODEX_HOME config.toml", async () => {
|
|
4461
|
+
await withLoreGuardConfig("0", "config-inline-enabled", async (cwd) => {
|
|
3808
4462
|
const result = await dispatchCodexNativeHook(
|
|
3809
4463
|
{
|
|
3810
4464
|
hook_event_name: "PreToolUse",
|
|
3811
4465
|
cwd,
|
|
3812
4466
|
tool_name: "Bash",
|
|
3813
|
-
tool_use_id: "tool-git-commit-lore-
|
|
3814
|
-
tool_input: { command: '
|
|
4467
|
+
tool_use_id: "tool-git-commit-lore-config-inline-enabled",
|
|
4468
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix: conventional"' },
|
|
3815
4469
|
},
|
|
3816
4470
|
{ cwd },
|
|
3817
4471
|
);
|
|
@@ -3819,36 +4473,185 @@ exit 0
|
|
|
3819
4473
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3820
4474
|
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3821
4475
|
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3822
|
-
}
|
|
3823
|
-
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3824
|
-
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3825
|
-
await rm(cwd, { recursive: true, force: true });
|
|
3826
|
-
}
|
|
4476
|
+
});
|
|
3827
4477
|
});
|
|
3828
4478
|
|
|
3829
|
-
it("
|
|
3830
|
-
|
|
3831
|
-
try {
|
|
4479
|
+
it("restores default-off Lore guard when env -u removes a disabled CODEX_HOME config source", async () => {
|
|
4480
|
+
await withLoreGuardConfig("0", "config-codex-home-unset", async (cwd) => {
|
|
3832
4481
|
const result = await dispatchCodexNativeHook(
|
|
3833
4482
|
{
|
|
3834
4483
|
hook_event_name: "PreToolUse",
|
|
3835
4484
|
cwd,
|
|
3836
4485
|
tool_name: "Bash",
|
|
3837
|
-
tool_use_id: "tool-git-commit-lore-
|
|
3838
|
-
tool_input: { command: '
|
|
4486
|
+
tool_use_id: "tool-git-commit-lore-config-codex-home-unset",
|
|
4487
|
+
tool_input: { command: 'env -u CODEX_HOME git commit -m "fix: conventional"' },
|
|
3839
4488
|
},
|
|
3840
4489
|
{ cwd },
|
|
3841
4490
|
);
|
|
3842
4491
|
|
|
3843
4492
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3844
|
-
assert.equal(
|
|
3845
|
-
|
|
3846
|
-
} finally {
|
|
3847
|
-
await rm(cwd, { recursive: true, force: true });
|
|
3848
|
-
}
|
|
4493
|
+
assert.equal(result.outputJson, null);
|
|
4494
|
+
});
|
|
3849
4495
|
});
|
|
3850
4496
|
|
|
3851
|
-
it("
|
|
4497
|
+
it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
|
|
4498
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
|
|
4499
|
+
try {
|
|
4500
|
+
const result = await dispatchCodexNativeHook(
|
|
4501
|
+
{
|
|
4502
|
+
hook_event_name: "PreToolUse",
|
|
4503
|
+
cwd,
|
|
4504
|
+
tool_name: "Bash",
|
|
4505
|
+
tool_use_id: "tool-git-commit-lore-inline-disabled",
|
|
4506
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
|
|
4507
|
+
},
|
|
4508
|
+
{ cwd },
|
|
4509
|
+
);
|
|
4510
|
+
|
|
4511
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4512
|
+
assert.equal(result.outputJson, null);
|
|
4513
|
+
} finally {
|
|
4514
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4517
|
+
|
|
4518
|
+
|
|
4519
|
+
it("allows inline disabled guard to override an enabled process env", async () => {
|
|
4520
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-override-disabled-"));
|
|
4521
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
4522
|
+
try {
|
|
4523
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
4524
|
+
const result = await dispatchCodexNativeHook(
|
|
4525
|
+
{
|
|
4526
|
+
hook_event_name: "PreToolUse",
|
|
4527
|
+
cwd,
|
|
4528
|
+
tool_name: "Bash",
|
|
4529
|
+
tool_use_id: "tool-git-commit-lore-inline-override-disabled",
|
|
4530
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
|
|
4531
|
+
},
|
|
4532
|
+
{ cwd },
|
|
4533
|
+
);
|
|
4534
|
+
|
|
4535
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4536
|
+
assert.equal(result.outputJson, null);
|
|
4537
|
+
} finally {
|
|
4538
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
4539
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
4540
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4541
|
+
}
|
|
4542
|
+
});
|
|
4543
|
+
|
|
4544
|
+
it("does not treat newline-separated Lore guard assignment as inline git commit opt-in", async () => {
|
|
4545
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
|
|
4546
|
+
try {
|
|
4547
|
+
const result = await dispatchCodexNativeHook(
|
|
4548
|
+
{
|
|
4549
|
+
hook_event_name: "PreToolUse",
|
|
4550
|
+
cwd,
|
|
4551
|
+
tool_name: "Bash",
|
|
4552
|
+
tool_use_id: "tool-git-commit-lore-newline-assignment",
|
|
4553
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1\ngit commit -m "fix: conventional"' },
|
|
4554
|
+
},
|
|
4555
|
+
{ cwd },
|
|
4556
|
+
);
|
|
4557
|
+
|
|
4558
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4559
|
+
assert.equal(result.outputJson, null);
|
|
4560
|
+
} finally {
|
|
4561
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4562
|
+
}
|
|
4563
|
+
});
|
|
4564
|
+
|
|
4565
|
+
it("restores default-off Lore guard when env -u unsets a config.toml fallback", async () => {
|
|
4566
|
+
await withLoreGuardConfig("1", "config-env-unset", async (cwd) => {
|
|
4567
|
+
const result = await dispatchCodexNativeHook(
|
|
4568
|
+
{
|
|
4569
|
+
hook_event_name: "PreToolUse",
|
|
4570
|
+
cwd,
|
|
4571
|
+
tool_name: "Bash",
|
|
4572
|
+
tool_use_id: "tool-git-commit-lore-config-env-unset",
|
|
4573
|
+
tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
|
|
4574
|
+
},
|
|
4575
|
+
{ cwd },
|
|
4576
|
+
);
|
|
4577
|
+
|
|
4578
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4579
|
+
assert.equal(result.outputJson, null);
|
|
4580
|
+
});
|
|
4581
|
+
});
|
|
4582
|
+
|
|
4583
|
+
it("restores default-off Lore guard when env -u unsets an enabled process env", async () => {
|
|
4584
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
|
|
4585
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
4586
|
+
try {
|
|
4587
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
4588
|
+
const result = await dispatchCodexNativeHook(
|
|
4589
|
+
{
|
|
4590
|
+
hook_event_name: "PreToolUse",
|
|
4591
|
+
cwd,
|
|
4592
|
+
tool_name: "Bash",
|
|
4593
|
+
tool_use_id: "tool-git-commit-lore-env-unset",
|
|
4594
|
+
tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
|
|
4595
|
+
},
|
|
4596
|
+
{ cwd },
|
|
4597
|
+
);
|
|
4598
|
+
|
|
4599
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4600
|
+
assert.equal(result.outputJson, null);
|
|
4601
|
+
} finally {
|
|
4602
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
4603
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
4604
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4605
|
+
}
|
|
4606
|
+
});
|
|
4607
|
+
|
|
4608
|
+
it("restores default-off Lore guard when env -i clears an enabled process env", async () => {
|
|
4609
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
|
|
4610
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
4611
|
+
try {
|
|
4612
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
4613
|
+
const result = await dispatchCodexNativeHook(
|
|
4614
|
+
{
|
|
4615
|
+
hook_event_name: "PreToolUse",
|
|
4616
|
+
cwd,
|
|
4617
|
+
tool_name: "Bash",
|
|
4618
|
+
tool_use_id: "tool-git-commit-lore-env-ignore",
|
|
4619
|
+
tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
|
|
4620
|
+
},
|
|
4621
|
+
{ cwd },
|
|
4622
|
+
);
|
|
4623
|
+
|
|
4624
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4625
|
+
assert.equal(result.outputJson, null);
|
|
4626
|
+
} finally {
|
|
4627
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
4628
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
4629
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4630
|
+
}
|
|
4631
|
+
});
|
|
4632
|
+
|
|
4633
|
+
it("keeps Lore commit enforcement disabled for unknown inline guard values", async () => {
|
|
4634
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
|
|
4635
|
+
try {
|
|
4636
|
+
const result = await dispatchCodexNativeHook(
|
|
4637
|
+
{
|
|
4638
|
+
hook_event_name: "PreToolUse",
|
|
4639
|
+
cwd,
|
|
4640
|
+
tool_name: "Bash",
|
|
4641
|
+
tool_use_id: "tool-git-commit-lore-inline-unknown",
|
|
4642
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
|
|
4643
|
+
},
|
|
4644
|
+
{ cwd },
|
|
4645
|
+
);
|
|
4646
|
+
|
|
4647
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4648
|
+
assert.equal(result.outputJson, null);
|
|
4649
|
+
} finally {
|
|
4650
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4651
|
+
}
|
|
4652
|
+
});
|
|
4653
|
+
|
|
4654
|
+
it("treats Lore commit guard disabled values as trim and case tolerant", async () => {
|
|
3852
4655
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-off-"));
|
|
3853
4656
|
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3854
4657
|
try {
|
|
@@ -3873,7 +4676,7 @@ exit 0
|
|
|
3873
4676
|
}
|
|
3874
4677
|
});
|
|
3875
4678
|
|
|
3876
|
-
it("keeps Lore commit enforcement
|
|
4679
|
+
it("keeps Lore commit enforcement disabled for unknown guard values", async () => {
|
|
3877
4680
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
|
|
3878
4681
|
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3879
4682
|
try {
|
|
@@ -3890,8 +4693,7 @@ exit 0
|
|
|
3890
4693
|
);
|
|
3891
4694
|
|
|
3892
4695
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3893
|
-
assert.equal(
|
|
3894
|
-
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
4696
|
+
assert.equal(result.outputJson, null);
|
|
3895
4697
|
} finally {
|
|
3896
4698
|
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3897
4699
|
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
@@ -3997,7 +4799,7 @@ exit 0
|
|
|
3997
4799
|
cwd,
|
|
3998
4800
|
tool_name: "Bash",
|
|
3999
4801
|
tool_use_id: "tool-git-commit-env-invalid",
|
|
4000
|
-
tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
|
|
4802
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 HUSKY=0 git commit -m "fix tests"' },
|
|
4001
4803
|
},
|
|
4002
4804
|
{ cwd },
|
|
4003
4805
|
);
|
|
@@ -4032,7 +4834,7 @@ exit 0
|
|
|
4032
4834
|
cwd,
|
|
4033
4835
|
tool_name: "Bash",
|
|
4034
4836
|
tool_use_id: "tool-git-commit-option-invalid",
|
|
4035
|
-
tool_input: { command: 'git -c core.editor=true commit -m "fix tests"' },
|
|
4837
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git -c core.editor=true commit -m "fix tests"' },
|
|
4036
4838
|
},
|
|
4037
4839
|
{ cwd },
|
|
4038
4840
|
);
|
|
@@ -4067,7 +4869,7 @@ exit 0
|
|
|
4067
4869
|
cwd,
|
|
4068
4870
|
tool_name: "Bash",
|
|
4069
4871
|
tool_use_id: "tool-git-exe-commit-env-wrapper-invalid",
|
|
4070
|
-
tool_input: { command: 'env git.exe commit -m "fix tests"' },
|
|
4872
|
+
tool_input: { command: 'env OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
4071
4873
|
},
|
|
4072
4874
|
{ cwd },
|
|
4073
4875
|
);
|
|
@@ -4102,7 +4904,7 @@ exit 0
|
|
|
4102
4904
|
cwd,
|
|
4103
4905
|
tool_name: "Bash",
|
|
4104
4906
|
tool_use_id: "tool-git-exe-commit-invalid",
|
|
4105
|
-
tool_input: { command: 'git.exe commit -m "fix tests"' },
|
|
4907
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
4106
4908
|
},
|
|
4107
4909
|
{ cwd },
|
|
4108
4910
|
);
|
|
@@ -4137,7 +4939,7 @@ exit 0
|
|
|
4137
4939
|
cwd,
|
|
4138
4940
|
tool_name: "Bash",
|
|
4139
4941
|
tool_use_id: "tool-git-exe-commit-env-flag-wrapper-invalid",
|
|
4140
|
-
tool_input: { command: 'env -i PATH=/usr/bin git.exe commit -m "fix tests"' },
|
|
4942
|
+
tool_input: { command: 'env -i PATH=/usr/bin OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
4141
4943
|
},
|
|
4142
4944
|
{ cwd },
|
|
4143
4945
|
);
|
|
@@ -4172,7 +4974,7 @@ exit 0
|
|
|
4172
4974
|
cwd,
|
|
4173
4975
|
tool_name: "Bash",
|
|
4174
4976
|
tool_use_id: "tool-git-exe-commit-env-value-wrapper-invalid",
|
|
4175
|
-
tool_input: { command: 'env -u FOO git.exe commit -m "fix tests"' },
|
|
4977
|
+
tool_input: { command: 'env -u FOO OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
4176
4978
|
},
|
|
4177
4979
|
{ cwd },
|
|
4178
4980
|
);
|
|
@@ -4207,7 +5009,7 @@ exit 0
|
|
|
4207
5009
|
cwd,
|
|
4208
5010
|
tool_name: "Bash",
|
|
4209
5011
|
tool_use_id: "tool-git-exe-commit-windows-path-invalid",
|
|
4210
|
-
tool_input: { command: '"C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
|
|
5012
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
|
|
4211
5013
|
},
|
|
4212
5014
|
{ cwd },
|
|
4213
5015
|
);
|
|
@@ -4242,7 +5044,7 @@ exit 0
|
|
|
4242
5044
|
cwd,
|
|
4243
5045
|
tool_name: "Bash",
|
|
4244
5046
|
tool_use_id: "tool-git-exe-commit-windows-backslash-path-invalid",
|
|
4245
|
-
tool_input: { command: '"C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
|
|
5047
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
|
|
4246
5048
|
},
|
|
4247
5049
|
{ cwd },
|
|
4248
5050
|
);
|
|
@@ -4277,7 +5079,7 @@ exit 0
|
|
|
4277
5079
|
cwd,
|
|
4278
5080
|
tool_name: "Bash",
|
|
4279
5081
|
tool_use_id: "tool-git-commit-path-invalid",
|
|
4280
|
-
tool_input: { command: '/usr/bin/git commit -m "fix tests"' },
|
|
5082
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 /usr/bin/git commit -m "fix tests"' },
|
|
4281
5083
|
},
|
|
4282
5084
|
{ cwd },
|
|
4283
5085
|
);
|
|
@@ -4312,7 +5114,7 @@ exit 0
|
|
|
4312
5114
|
cwd,
|
|
4313
5115
|
tool_name: "Bash",
|
|
4314
5116
|
tool_use_id: "tool-git-commit-file",
|
|
4315
|
-
tool_input: { command: "git commit -F .git/COMMIT_EDITMSG" },
|
|
5117
|
+
tool_input: { command: "OMX_LORE_COMMIT_GUARD=1 git commit -F .git/COMMIT_EDITMSG" },
|
|
4316
5118
|
},
|
|
4317
5119
|
{ cwd },
|
|
4318
5120
|
);
|
|
@@ -4346,7 +5148,7 @@ exit 0
|
|
|
4346
5148
|
tool_use_id: "tool-git-commit-missing-omx-coauthor",
|
|
4347
5149
|
tool_input: {
|
|
4348
5150
|
command: [
|
|
4349
|
-
'git commit',
|
|
5151
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
4350
5152
|
'-m "Prevent invalid history from bypassing Lore enforcement"',
|
|
4351
5153
|
'-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
|
|
4352
5154
|
'-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
|
|
@@ -4386,7 +5188,7 @@ exit 0
|
|
|
4386
5188
|
tool_use_id: "tool-git-commit-valid",
|
|
4387
5189
|
tool_input: {
|
|
4388
5190
|
command: [
|
|
4389
|
-
'git commit',
|
|
5191
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
4390
5192
|
'-m "Prevent invalid history from bypassing Lore enforcement"',
|
|
4391
5193
|
'-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
|
|
4392
5194
|
'-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
|
|
@@ -4416,7 +5218,7 @@ exit 0
|
|
|
4416
5218
|
tool_use_id: "tool-git-commit-compact-coauthor",
|
|
4417
5219
|
tool_input: {
|
|
4418
5220
|
command: [
|
|
4419
|
-
'git commit',
|
|
5221
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
4420
5222
|
'-m "Launch lvisai.xyz intro site"',
|
|
4421
5223
|
'-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
4422
5224
|
].join(" "),
|
|
@@ -4443,7 +5245,7 @@ exit 0
|
|
|
4443
5245
|
tool_use_id: "tool-git-commit-compact-trailers",
|
|
4444
5246
|
tool_input: {
|
|
4445
5247
|
command: [
|
|
4446
|
-
'git commit',
|
|
5248
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
4447
5249
|
'-m "Launch lvisai.xyz intro site"',
|
|
4448
5250
|
'-m "Constraint: Native PreToolUse can only inspect inline Bash command text\nTested: node --test dist/scripts/__tests__/codex-native-hook.test.js\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
4449
5251
|
].join(" "),
|
|
@@ -4470,7 +5272,7 @@ exit 0
|
|
|
4470
5272
|
tool_use_id: "tool-git-commit-compact-no-separator",
|
|
4471
5273
|
tool_input: {
|
|
4472
5274
|
command: [
|
|
4473
|
-
'git commit',
|
|
5275
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
4474
5276
|
'--message="Launch lvisai.xyz intro site\nCo-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
4475
5277
|
].join(" "),
|
|
4476
5278
|
},
|
|
@@ -4866,6 +5668,99 @@ exit 0
|
|
|
4866
5668
|
}
|
|
4867
5669
|
});
|
|
4868
5670
|
|
|
5671
|
+
it("does not treat non-MCP source output containing detector constants as MCP transport death", async () => {
|
|
5672
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-read-mcp-source-"));
|
|
5673
|
+
try {
|
|
5674
|
+
const result = await dispatchCodexNativeHook(
|
|
5675
|
+
{
|
|
5676
|
+
hook_event_name: "PostToolUse",
|
|
5677
|
+
cwd,
|
|
5678
|
+
tool_name: "Read",
|
|
5679
|
+
tool_use_id: "tool-read-mcp-source",
|
|
5680
|
+
tool_input: { file_path: "src/scripts/codex-native-pre-post.ts" },
|
|
5681
|
+
tool_response:
|
|
5682
|
+
"const MCP_TRANSPORT_FAILURE_PATTERNS = [/transport closed/i, /server disconnected/i];\nconst context = /\\bmcp\\b/i;",
|
|
5683
|
+
},
|
|
5684
|
+
{ cwd },
|
|
5685
|
+
);
|
|
5686
|
+
|
|
5687
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5688
|
+
assert.equal(result.outputJson, null);
|
|
5689
|
+
} finally {
|
|
5690
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5691
|
+
}
|
|
5692
|
+
});
|
|
5693
|
+
|
|
5694
|
+
it("does not treat non-MCP docs stdout mentioning closed MCP transport as transport death", async () => {
|
|
5695
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-docs-mcp-log-"));
|
|
5696
|
+
try {
|
|
5697
|
+
const result = await dispatchCodexNativeHook(
|
|
5698
|
+
{
|
|
5699
|
+
hook_event_name: "PostToolUse",
|
|
5700
|
+
cwd,
|
|
5701
|
+
tool_name: "ShellOutput",
|
|
5702
|
+
tool_use_id: "tool-docs-mcp-log",
|
|
5703
|
+
tool_response: JSON.stringify({
|
|
5704
|
+
stdout: "Troubleshooting note: MCP transport closed after the server disconnected in an old log.",
|
|
5705
|
+
stderr: "",
|
|
5706
|
+
}),
|
|
5707
|
+
},
|
|
5708
|
+
{ cwd },
|
|
5709
|
+
);
|
|
5710
|
+
|
|
5711
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5712
|
+
assert.equal(result.outputJson, null);
|
|
5713
|
+
} finally {
|
|
5714
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5715
|
+
}
|
|
5716
|
+
});
|
|
5717
|
+
|
|
5718
|
+
it("does not MCP-block non-MCP command output with unrelated stderr and MCP transport stdout", async () => {
|
|
5719
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-nonmcp-mixed-output-"));
|
|
5720
|
+
try {
|
|
5721
|
+
const result = await dispatchCodexNativeHook(
|
|
5722
|
+
{
|
|
5723
|
+
hook_event_name: "PostToolUse",
|
|
5724
|
+
cwd,
|
|
5725
|
+
tool_name: "ShellOutput",
|
|
5726
|
+
tool_use_id: "tool-nonmcp-mixed-output",
|
|
5727
|
+
tool_response: JSON.stringify({
|
|
5728
|
+
stdout: "captured log line: MCP transport closed before response",
|
|
5729
|
+
stderr: "grep: fixture.txt: No such file or directory",
|
|
5730
|
+
}),
|
|
5731
|
+
},
|
|
5732
|
+
{ cwd },
|
|
5733
|
+
);
|
|
5734
|
+
|
|
5735
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5736
|
+
assert.equal(result.outputJson, null);
|
|
5737
|
+
} finally {
|
|
5738
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5739
|
+
}
|
|
5740
|
+
});
|
|
5741
|
+
|
|
5742
|
+
it("still blocks MCP-like raw transport failures", async () => {
|
|
5743
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-raw-transport-"));
|
|
5744
|
+
try {
|
|
5745
|
+
const result = await dispatchCodexNativeHook(
|
|
5746
|
+
{
|
|
5747
|
+
hook_event_name: "PostToolUse",
|
|
5748
|
+
cwd,
|
|
5749
|
+
tool_name: "mcp__omx_state__state_write",
|
|
5750
|
+
tool_use_id: "tool-mcp-raw-transport",
|
|
5751
|
+
tool_response: "transport closed after server disconnected",
|
|
5752
|
+
},
|
|
5753
|
+
{ cwd },
|
|
5754
|
+
);
|
|
5755
|
+
|
|
5756
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5757
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
5758
|
+
assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
|
|
5759
|
+
} finally {
|
|
5760
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5761
|
+
}
|
|
5762
|
+
});
|
|
5763
|
+
|
|
4869
5764
|
it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
|
|
4870
5765
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
|
|
4871
5766
|
try {
|
|
@@ -5313,6 +6208,61 @@ exit 0
|
|
|
5313
6208
|
}
|
|
5314
6209
|
});
|
|
5315
6210
|
|
|
6211
|
+
it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
|
|
6212
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
|
|
6213
|
+
try {
|
|
6214
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6215
|
+
const sessionId = "sess-stop-autopilot-review-compact";
|
|
6216
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6217
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
6218
|
+
active: true,
|
|
6219
|
+
mode: "autopilot",
|
|
6220
|
+
current_phase: "code-review",
|
|
6221
|
+
state: {
|
|
6222
|
+
phase_cycle: ["ralplan", "ralph", "code-review"],
|
|
6223
|
+
handoff_artifacts: {
|
|
6224
|
+
ralplan: ".omx/plans/prd-issue-2366.md",
|
|
6225
|
+
ralph: { verification: ["npm test"] },
|
|
6226
|
+
code_review: null,
|
|
6227
|
+
},
|
|
6228
|
+
review_verdict: null,
|
|
6229
|
+
},
|
|
6230
|
+
});
|
|
6231
|
+
|
|
6232
|
+
const compactBoundary = await dispatchCodexNativeHook(
|
|
6233
|
+
{
|
|
6234
|
+
hook_event_name: "Stop",
|
|
6235
|
+
cwd,
|
|
6236
|
+
session_id: sessionId,
|
|
6237
|
+
stop_reason: "context compact",
|
|
6238
|
+
},
|
|
6239
|
+
{ cwd },
|
|
6240
|
+
);
|
|
6241
|
+
const resumedStop = await dispatchCodexNativeHook(
|
|
6242
|
+
{
|
|
6243
|
+
hook_event_name: "Stop",
|
|
6244
|
+
cwd,
|
|
6245
|
+
session_id: sessionId,
|
|
6246
|
+
},
|
|
6247
|
+
{ cwd },
|
|
6248
|
+
);
|
|
6249
|
+
|
|
6250
|
+
assert.equal(compactBoundary.omxEventName, "stop");
|
|
6251
|
+
assert.equal(compactBoundary.outputJson, null);
|
|
6252
|
+
assert.equal(resumedStop.omxEventName, "stop");
|
|
6253
|
+
assert.deepEqual(resumedStop.outputJson, {
|
|
6254
|
+
decision: "block",
|
|
6255
|
+
reason:
|
|
6256
|
+
"OMX autopilot is still active (phase: code-review); continue the task and gather fresh verification evidence before stopping.",
|
|
6257
|
+
stopReason: "autopilot_code-review",
|
|
6258
|
+
systemMessage:
|
|
6259
|
+
"OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state.",
|
|
6260
|
+
});
|
|
6261
|
+
} finally {
|
|
6262
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6263
|
+
}
|
|
6264
|
+
});
|
|
6265
|
+
|
|
5316
6266
|
it("suppresses duplicate Autopilot planning Stop replays so stale planning state cannot loop indefinitely", async () => {
|
|
5317
6267
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
|
|
5318
6268
|
try {
|
|
@@ -5355,6 +6305,91 @@ exit 0
|
|
|
5355
6305
|
}
|
|
5356
6306
|
});
|
|
5357
6307
|
|
|
6308
|
+
it("allows Stop when terminal Autopilot run-state shadows stale session ralplan state", async () => {
|
|
6309
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-terminal-run-state-"));
|
|
6310
|
+
try {
|
|
6311
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6312
|
+
const sessionId = "sess-stop-autopilot-terminal-run-state";
|
|
6313
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6314
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
6315
|
+
active: true,
|
|
6316
|
+
mode: "autopilot",
|
|
6317
|
+
current_phase: "ralplan",
|
|
6318
|
+
});
|
|
6319
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
6320
|
+
version: 1,
|
|
6321
|
+
active: false,
|
|
6322
|
+
mode: "autopilot",
|
|
6323
|
+
outcome: "finish",
|
|
6324
|
+
lifecycle_outcome: "finished",
|
|
6325
|
+
current_phase: "complete",
|
|
6326
|
+
completed_at: "2026-05-20T11:00:00.000Z",
|
|
6327
|
+
updated_at: "2026-05-20T11:00:00.000Z",
|
|
6328
|
+
});
|
|
6329
|
+
|
|
6330
|
+
const result = await dispatchCodexNativeHook(
|
|
6331
|
+
{
|
|
6332
|
+
hook_event_name: "Stop",
|
|
6333
|
+
cwd,
|
|
6334
|
+
session_id: sessionId,
|
|
6335
|
+
thread_id: "thread-stop-autopilot-terminal-run-state",
|
|
6336
|
+
turn_id: "turn-stop-autopilot-terminal-run-state-1",
|
|
6337
|
+
last_assistant_message: "Done. Verification passed.",
|
|
6338
|
+
},
|
|
6339
|
+
{ cwd },
|
|
6340
|
+
);
|
|
6341
|
+
|
|
6342
|
+
assert.equal(result.omxEventName, "stop");
|
|
6343
|
+
assert.equal(result.outputJson, null);
|
|
6344
|
+
} finally {
|
|
6345
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6346
|
+
}
|
|
6347
|
+
});
|
|
6348
|
+
|
|
6349
|
+
it("still blocks Stop while Autopilot ralplan state is genuinely non-terminal", async () => {
|
|
6350
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-active-ralplan-"));
|
|
6351
|
+
try {
|
|
6352
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6353
|
+
const sessionId = "sess-stop-autopilot-active-ralplan";
|
|
6354
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6355
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
6356
|
+
active: true,
|
|
6357
|
+
mode: "autopilot",
|
|
6358
|
+
current_phase: "ralplan",
|
|
6359
|
+
});
|
|
6360
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
6361
|
+
version: 1,
|
|
6362
|
+
active: true,
|
|
6363
|
+
mode: "autopilot",
|
|
6364
|
+
outcome: "continue",
|
|
6365
|
+
current_phase: "ralplan",
|
|
6366
|
+
updated_at: "2026-05-20T11:00:00.000Z",
|
|
6367
|
+
});
|
|
6368
|
+
|
|
6369
|
+
const result = await dispatchCodexNativeHook(
|
|
6370
|
+
{
|
|
6371
|
+
hook_event_name: "Stop",
|
|
6372
|
+
cwd,
|
|
6373
|
+
session_id: sessionId,
|
|
6374
|
+
thread_id: "thread-stop-autopilot-active-ralplan",
|
|
6375
|
+
turn_id: "turn-stop-autopilot-active-ralplan-1",
|
|
6376
|
+
},
|
|
6377
|
+
{ cwd },
|
|
6378
|
+
);
|
|
6379
|
+
|
|
6380
|
+
assert.equal(result.omxEventName, "stop");
|
|
6381
|
+
assert.deepEqual(result.outputJson, {
|
|
6382
|
+
decision: "block",
|
|
6383
|
+
reason:
|
|
6384
|
+
"OMX autopilot is still active (phase: ralplan); continue the task and gather fresh verification evidence before stopping.",
|
|
6385
|
+
stopReason: "autopilot_ralplan",
|
|
6386
|
+
systemMessage: "OMX autopilot is still active (phase: ralplan).",
|
|
6387
|
+
});
|
|
6388
|
+
} finally {
|
|
6389
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6390
|
+
}
|
|
6391
|
+
});
|
|
6392
|
+
|
|
5358
6393
|
it("does not block Stop from stale root Autopilot planning state when the explicit session has no scoped state", async () => {
|
|
5359
6394
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autopilot-planning-"));
|
|
5360
6395
|
try {
|
|
@@ -5913,6 +6948,98 @@ exit 0
|
|
|
5913
6948
|
}
|
|
5914
6949
|
});
|
|
5915
6950
|
|
|
6951
|
+
it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
|
|
6952
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
|
|
6953
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
6954
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
6955
|
+
const prevPath = process.env.PATH;
|
|
6956
|
+
try {
|
|
6957
|
+
await initTeamState(
|
|
6958
|
+
"worker-stop-team-busy-leader",
|
|
6959
|
+
"worker stop busy leader",
|
|
6960
|
+
"executor",
|
|
6961
|
+
1,
|
|
6962
|
+
cwd,
|
|
6963
|
+
undefined,
|
|
6964
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-busy-leader" },
|
|
6965
|
+
);
|
|
6966
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
6967
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
6968
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
6969
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { busyLeader: true }));
|
|
6970
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
6971
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6972
|
+
const teamDir = join(stateDir, "team", "worker-stop-team-busy-leader");
|
|
6973
|
+
const workerDir = join(teamDir, "workers", "worker-1");
|
|
6974
|
+
await writeJson(join(teamDir, "config.json"), {
|
|
6975
|
+
name: "worker-stop-team-busy-leader",
|
|
6976
|
+
tmux_session: "omx-team-worker-stop",
|
|
6977
|
+
leader_pane_id: "%42",
|
|
6978
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
6979
|
+
});
|
|
6980
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
6981
|
+
name: "worker-stop-team-busy-leader",
|
|
6982
|
+
tmux_session: "omx-team-worker-stop",
|
|
6983
|
+
leader_pane_id: "%42",
|
|
6984
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
6985
|
+
});
|
|
6986
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
6987
|
+
name: "worker-1",
|
|
6988
|
+
index: 1,
|
|
6989
|
+
role: "executor",
|
|
6990
|
+
assigned_tasks: ["1"],
|
|
6991
|
+
worktree_path: cwd,
|
|
6992
|
+
team_state_root: stateDir,
|
|
6993
|
+
});
|
|
6994
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
6995
|
+
state: "done",
|
|
6996
|
+
current_task_id: "1",
|
|
6997
|
+
updated_at: new Date().toISOString(),
|
|
6998
|
+
});
|
|
6999
|
+
await writeJson(join(teamDir, "tasks", "task-1.json"), {
|
|
7000
|
+
id: "1",
|
|
7001
|
+
subject: "hook task",
|
|
7002
|
+
description: "finish hook task",
|
|
7003
|
+
status: "completed",
|
|
7004
|
+
owner: "worker-1",
|
|
7005
|
+
created_at: new Date().toISOString(),
|
|
7006
|
+
});
|
|
7007
|
+
|
|
7008
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-team-busy-leader/worker-1";
|
|
7009
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
7010
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7011
|
+
|
|
7012
|
+
const result = await dispatchCodexNativeHook(
|
|
7013
|
+
{
|
|
7014
|
+
hook_event_name: "Stop",
|
|
7015
|
+
cwd,
|
|
7016
|
+
session_id: "sess-stop-team-worker-busy-leader",
|
|
7017
|
+
},
|
|
7018
|
+
{ cwd },
|
|
7019
|
+
);
|
|
7020
|
+
|
|
7021
|
+
assert.equal(result.outputJson, null);
|
|
7022
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7023
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
7024
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
7025
|
+
assert.match(tmuxLog, /send-keys -t %42 C-m/);
|
|
7026
|
+
assert.ok(
|
|
7027
|
+
tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
|
|
7028
|
+
"busy worker-stop nudge should press Tab before C-m",
|
|
7029
|
+
);
|
|
7030
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
7031
|
+
assert.equal(nudgeState.delivery, "queued");
|
|
7032
|
+
} finally {
|
|
7033
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
7034
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
7035
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
7036
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
7037
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
7038
|
+
else delete process.env.PATH;
|
|
7039
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7040
|
+
}
|
|
7041
|
+
});
|
|
7042
|
+
|
|
5916
7043
|
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
5917
7044
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
5918
7045
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
@@ -6843,13 +7970,25 @@ exit 0
|
|
|
6843
7970
|
}
|
|
6844
7971
|
});
|
|
6845
7972
|
|
|
6846
|
-
it("does not block
|
|
6847
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
7973
|
+
it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
|
|
7974
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
|
|
6848
7975
|
try {
|
|
6849
7976
|
const stateDir = join(cwd, ".omx", "state");
|
|
6850
|
-
const sessionId = "sess-stop-
|
|
7977
|
+
const sessionId = "sess-stop-stale-session-ralplan";
|
|
6851
7978
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6852
7979
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
7980
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
7981
|
+
active: false,
|
|
7982
|
+
skill: "ralplan",
|
|
7983
|
+
phase: "reviewing",
|
|
7984
|
+
active_skills: [],
|
|
7985
|
+
});
|
|
7986
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
7987
|
+
active: false,
|
|
7988
|
+
mode: "ralplan",
|
|
7989
|
+
current_phase: "complete",
|
|
7990
|
+
session_id: sessionId,
|
|
7991
|
+
});
|
|
6853
7992
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6854
7993
|
active: true,
|
|
6855
7994
|
skill: "ralplan",
|
|
@@ -6868,16 +8007,6 @@ exit 0
|
|
|
6868
8007
|
current_phase: "planning",
|
|
6869
8008
|
session_id: sessionId,
|
|
6870
8009
|
});
|
|
6871
|
-
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
6872
|
-
version: 1,
|
|
6873
|
-
mode: "ralplan",
|
|
6874
|
-
active: false,
|
|
6875
|
-
outcome: "finish",
|
|
6876
|
-
lifecycle_outcome: "finished",
|
|
6877
|
-
current_phase: "complete",
|
|
6878
|
-
completed_at: "2026-05-01T00:00:00.000Z",
|
|
6879
|
-
updated_at: "2026-05-01T00:00:00.000Z",
|
|
6880
|
-
});
|
|
6881
8010
|
|
|
6882
8011
|
const result = await dispatchCodexNativeHook(
|
|
6883
8012
|
{
|
|
@@ -6895,13 +8024,26 @@ exit 0
|
|
|
6895
8024
|
}
|
|
6896
8025
|
});
|
|
6897
8026
|
|
|
6898
|
-
it("
|
|
6899
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
8027
|
+
it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
|
|
8028
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
|
|
6900
8029
|
try {
|
|
6901
8030
|
const stateDir = join(cwd, ".omx", "state");
|
|
6902
|
-
const sessionId = "sess-stop-current-ralplan";
|
|
8031
|
+
const sessionId = "sess-stop-current-active-ralplan";
|
|
6903
8032
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6904
8033
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
8034
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
8035
|
+
active: false,
|
|
8036
|
+
skill: "ralplan",
|
|
8037
|
+
phase: "complete",
|
|
8038
|
+
session_id: "sess-stop-old-ralplan",
|
|
8039
|
+
active_skills: [],
|
|
8040
|
+
});
|
|
8041
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
8042
|
+
active: false,
|
|
8043
|
+
mode: "ralplan",
|
|
8044
|
+
current_phase: "complete",
|
|
8045
|
+
session_id: "sess-stop-old-ralplan",
|
|
8046
|
+
});
|
|
6905
8047
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6906
8048
|
active: true,
|
|
6907
8049
|
skill: "ralplan",
|
|
@@ -6918,7 +8060,7 @@ exit 0
|
|
|
6918
8060
|
active: true,
|
|
6919
8061
|
mode: "ralplan",
|
|
6920
8062
|
current_phase: "planning",
|
|
6921
|
-
session_id:
|
|
8063
|
+
session_id: sessionId,
|
|
6922
8064
|
});
|
|
6923
8065
|
|
|
6924
8066
|
const result = await dispatchCodexNativeHook(
|
|
@@ -6931,85 +8073,349 @@ exit 0
|
|
|
6931
8073
|
);
|
|
6932
8074
|
|
|
6933
8075
|
assert.equal(result.omxEventName, "stop");
|
|
6934
|
-
assert.equal(result.outputJson,
|
|
8076
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
8077
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
6935
8078
|
} finally {
|
|
6936
8079
|
await rm(cwd, { recursive: true, force: true });
|
|
6937
8080
|
}
|
|
6938
8081
|
});
|
|
6939
8082
|
|
|
6940
|
-
it("
|
|
6941
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
8083
|
+
it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
|
|
8084
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
|
|
6942
8085
|
try {
|
|
6943
8086
|
const stateDir = join(cwd, ".omx", "state");
|
|
6944
|
-
|
|
6945
|
-
await
|
|
6946
|
-
await writeJson(join(stateDir, "
|
|
8087
|
+
const sessionId = "sess-stop-unscoped-root-current-active";
|
|
8088
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
8089
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
8090
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
8091
|
+
active: false,
|
|
8092
|
+
skill: "ralplan",
|
|
8093
|
+
phase: "complete",
|
|
8094
|
+
active_skills: [],
|
|
8095
|
+
});
|
|
8096
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
8097
|
+
active: false,
|
|
8098
|
+
mode: "ralplan",
|
|
8099
|
+
current_phase: "complete",
|
|
8100
|
+
});
|
|
8101
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6947
8102
|
active: true,
|
|
6948
8103
|
skill: "ralplan",
|
|
6949
8104
|
phase: "planning",
|
|
8105
|
+
session_id: sessionId,
|
|
8106
|
+
active_skills: [{
|
|
8107
|
+
skill: "ralplan",
|
|
8108
|
+
phase: "planning",
|
|
8109
|
+
active: true,
|
|
8110
|
+
session_id: sessionId,
|
|
8111
|
+
}],
|
|
6950
8112
|
});
|
|
6951
|
-
await writeJson(join(stateDir, "sessions",
|
|
8113
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6952
8114
|
active: true,
|
|
8115
|
+
mode: "ralplan",
|
|
6953
8116
|
current_phase: "planning",
|
|
6954
|
-
|
|
6955
|
-
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
6956
|
-
schemaVersion: 1,
|
|
6957
|
-
sessions: {
|
|
6958
|
-
"sess-stop-skill-subagent": {
|
|
6959
|
-
session_id: "sess-stop-skill-subagent",
|
|
6960
|
-
leader_thread_id: "leader-1",
|
|
6961
|
-
updated_at: new Date().toISOString(),
|
|
6962
|
-
threads: {
|
|
6963
|
-
"leader-1": {
|
|
6964
|
-
thread_id: "leader-1",
|
|
6965
|
-
kind: "leader",
|
|
6966
|
-
first_seen_at: new Date().toISOString(),
|
|
6967
|
-
last_seen_at: new Date().toISOString(),
|
|
6968
|
-
turn_count: 1,
|
|
6969
|
-
},
|
|
6970
|
-
"sub-1": {
|
|
6971
|
-
thread_id: "sub-1",
|
|
6972
|
-
kind: "subagent",
|
|
6973
|
-
first_seen_at: new Date().toISOString(),
|
|
6974
|
-
last_seen_at: new Date().toISOString(),
|
|
6975
|
-
turn_count: 1,
|
|
6976
|
-
},
|
|
6977
|
-
},
|
|
6978
|
-
},
|
|
6979
|
-
},
|
|
8117
|
+
session_id: sessionId,
|
|
6980
8118
|
});
|
|
6981
8119
|
|
|
6982
8120
|
const result = await dispatchCodexNativeHook(
|
|
6983
8121
|
{
|
|
6984
8122
|
hook_event_name: "Stop",
|
|
6985
8123
|
cwd,
|
|
6986
|
-
session_id:
|
|
8124
|
+
session_id: sessionId,
|
|
6987
8125
|
},
|
|
6988
8126
|
{ cwd },
|
|
6989
8127
|
);
|
|
6990
8128
|
|
|
6991
8129
|
assert.equal(result.omxEventName, "stop");
|
|
6992
8130
|
assert.equal(result.outputJson?.decision, "block");
|
|
6993
|
-
assert.
|
|
6994
|
-
assert.match(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
|
|
6995
|
-
assert.match(String(result.outputJson?.reason ?? ""), /then continue from the current ralplan artifact/i);
|
|
6996
|
-
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_waiting_subagent");
|
|
8131
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
6997
8132
|
} finally {
|
|
6998
8133
|
await rm(cwd, { recursive: true, force: true });
|
|
6999
8134
|
}
|
|
7000
8135
|
});
|
|
7001
8136
|
|
|
7002
|
-
it("does not block
|
|
7003
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-"));
|
|
8137
|
+
it("does not block stale session ralplan when root ralplan is terminal and another root skill is active", async () => {
|
|
8138
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-ralplan-other-root-skill-"));
|
|
7004
8139
|
try {
|
|
7005
8140
|
const stateDir = join(cwd, ".omx", "state");
|
|
7006
|
-
|
|
8141
|
+
const sessionId = "sess-stop-stale-ralplan-other-root-skill";
|
|
8142
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
8143
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
7007
8144
|
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
8145
|
+
active: true,
|
|
8146
|
+
skill: "deep-interview",
|
|
8147
|
+
phase: "intent-first",
|
|
8148
|
+
session_id: sessionId,
|
|
8149
|
+
active_skills: [{
|
|
8150
|
+
skill: "deep-interview",
|
|
8151
|
+
phase: "intent-first",
|
|
8152
|
+
active: true,
|
|
8153
|
+
session_id: sessionId,
|
|
8154
|
+
}],
|
|
8155
|
+
});
|
|
8156
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
8157
|
+
active: false,
|
|
8158
|
+
mode: "ralplan",
|
|
8159
|
+
current_phase: "complete",
|
|
8160
|
+
session_id: sessionId,
|
|
8161
|
+
});
|
|
8162
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7008
8163
|
active: true,
|
|
7009
8164
|
skill: "ralplan",
|
|
7010
8165
|
phase: "planning",
|
|
7011
|
-
|
|
7012
|
-
|
|
8166
|
+
session_id: sessionId,
|
|
8167
|
+
active_skills: [{
|
|
8168
|
+
skill: "ralplan",
|
|
8169
|
+
phase: "planning",
|
|
8170
|
+
active: true,
|
|
8171
|
+
session_id: sessionId,
|
|
8172
|
+
}],
|
|
8173
|
+
});
|
|
8174
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
8175
|
+
active: true,
|
|
8176
|
+
mode: "ralplan",
|
|
8177
|
+
current_phase: "planning",
|
|
8178
|
+
session_id: sessionId,
|
|
8179
|
+
});
|
|
8180
|
+
|
|
8181
|
+
const result = await dispatchCodexNativeHook(
|
|
8182
|
+
{
|
|
8183
|
+
hook_event_name: "Stop",
|
|
8184
|
+
cwd,
|
|
8185
|
+
session_id: sessionId,
|
|
8186
|
+
},
|
|
8187
|
+
{ cwd },
|
|
8188
|
+
);
|
|
8189
|
+
|
|
8190
|
+
assert.equal(result.omxEventName, "stop");
|
|
8191
|
+
assert.equal(result.outputJson, null);
|
|
8192
|
+
} finally {
|
|
8193
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8194
|
+
}
|
|
8195
|
+
});
|
|
8196
|
+
|
|
8197
|
+
it("keeps blocking session ralplan when canonical root state is not inactive", async () => {
|
|
8198
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-active-"));
|
|
8199
|
+
try {
|
|
8200
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8201
|
+
const sessionId = "sess-stop-session-ralplan-root-active";
|
|
8202
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
8203
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
8204
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
8205
|
+
active: true,
|
|
8206
|
+
skill: "ralplan",
|
|
8207
|
+
phase: "planning",
|
|
8208
|
+
session_id: sessionId,
|
|
8209
|
+
active_skills: [{
|
|
8210
|
+
skill: "ralplan",
|
|
8211
|
+
phase: "planning",
|
|
8212
|
+
active: true,
|
|
8213
|
+
session_id: sessionId,
|
|
8214
|
+
}],
|
|
8215
|
+
});
|
|
8216
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
8217
|
+
active: true,
|
|
8218
|
+
skill: "ralplan",
|
|
8219
|
+
phase: "planning",
|
|
8220
|
+
session_id: sessionId,
|
|
8221
|
+
active_skills: [{
|
|
8222
|
+
skill: "ralplan",
|
|
8223
|
+
phase: "planning",
|
|
8224
|
+
active: true,
|
|
8225
|
+
session_id: sessionId,
|
|
8226
|
+
}],
|
|
8227
|
+
});
|
|
8228
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
8229
|
+
active: true,
|
|
8230
|
+
mode: "ralplan",
|
|
8231
|
+
current_phase: "planning",
|
|
8232
|
+
session_id: sessionId,
|
|
8233
|
+
});
|
|
8234
|
+
|
|
8235
|
+
const result = await dispatchCodexNativeHook(
|
|
8236
|
+
{
|
|
8237
|
+
hook_event_name: "Stop",
|
|
8238
|
+
cwd,
|
|
8239
|
+
session_id: sessionId,
|
|
8240
|
+
},
|
|
8241
|
+
{ cwd },
|
|
8242
|
+
);
|
|
8243
|
+
|
|
8244
|
+
assert.equal(result.omxEventName, "stop");
|
|
8245
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
8246
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
8247
|
+
} finally {
|
|
8248
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8249
|
+
}
|
|
8250
|
+
});
|
|
8251
|
+
|
|
8252
|
+
it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
|
|
8253
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
|
|
8254
|
+
try {
|
|
8255
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8256
|
+
const sessionId = "sess-stop-terminal-ralplan";
|
|
8257
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
8258
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
8259
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
8260
|
+
active: true,
|
|
8261
|
+
skill: "ralplan",
|
|
8262
|
+
phase: "planning",
|
|
8263
|
+
session_id: sessionId,
|
|
8264
|
+
active_skills: [{
|
|
8265
|
+
skill: "ralplan",
|
|
8266
|
+
phase: "planning",
|
|
8267
|
+
active: true,
|
|
8268
|
+
session_id: sessionId,
|
|
8269
|
+
}],
|
|
8270
|
+
});
|
|
8271
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
8272
|
+
active: true,
|
|
8273
|
+
mode: "ralplan",
|
|
8274
|
+
current_phase: "planning",
|
|
8275
|
+
session_id: sessionId,
|
|
8276
|
+
});
|
|
8277
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
8278
|
+
version: 1,
|
|
8279
|
+
mode: "ralplan",
|
|
8280
|
+
active: false,
|
|
8281
|
+
outcome: "finish",
|
|
8282
|
+
lifecycle_outcome: "finished",
|
|
8283
|
+
current_phase: "complete",
|
|
8284
|
+
completed_at: "2026-05-01T00:00:00.000Z",
|
|
8285
|
+
updated_at: "2026-05-01T00:00:00.000Z",
|
|
8286
|
+
});
|
|
8287
|
+
|
|
8288
|
+
const result = await dispatchCodexNativeHook(
|
|
8289
|
+
{
|
|
8290
|
+
hook_event_name: "Stop",
|
|
8291
|
+
cwd,
|
|
8292
|
+
session_id: sessionId,
|
|
8293
|
+
},
|
|
8294
|
+
{ cwd },
|
|
8295
|
+
);
|
|
8296
|
+
|
|
8297
|
+
assert.equal(result.omxEventName, "stop");
|
|
8298
|
+
assert.equal(result.outputJson, null);
|
|
8299
|
+
} finally {
|
|
8300
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8301
|
+
}
|
|
8302
|
+
});
|
|
8303
|
+
|
|
8304
|
+
it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
|
|
8305
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
|
|
8306
|
+
try {
|
|
8307
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8308
|
+
const sessionId = "sess-stop-current-ralplan";
|
|
8309
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
8310
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
8311
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
8312
|
+
active: true,
|
|
8313
|
+
skill: "ralplan",
|
|
8314
|
+
phase: "planning",
|
|
8315
|
+
session_id: sessionId,
|
|
8316
|
+
active_skills: [{
|
|
8317
|
+
skill: "ralplan",
|
|
8318
|
+
phase: "planning",
|
|
8319
|
+
active: true,
|
|
8320
|
+
session_id: sessionId,
|
|
8321
|
+
}],
|
|
8322
|
+
});
|
|
8323
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
8324
|
+
active: true,
|
|
8325
|
+
mode: "ralplan",
|
|
8326
|
+
current_phase: "planning",
|
|
8327
|
+
session_id: "sess-other-ralplan",
|
|
8328
|
+
});
|
|
8329
|
+
|
|
8330
|
+
const result = await dispatchCodexNativeHook(
|
|
8331
|
+
{
|
|
8332
|
+
hook_event_name: "Stop",
|
|
8333
|
+
cwd,
|
|
8334
|
+
session_id: sessionId,
|
|
8335
|
+
},
|
|
8336
|
+
{ cwd },
|
|
8337
|
+
);
|
|
8338
|
+
|
|
8339
|
+
assert.equal(result.omxEventName, "stop");
|
|
8340
|
+
assert.equal(result.outputJson, null);
|
|
8341
|
+
} finally {
|
|
8342
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8343
|
+
}
|
|
8344
|
+
});
|
|
8345
|
+
|
|
8346
|
+
it("returns an explicit ralplan waiting status while subagents are still active", async () => {
|
|
8347
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
|
|
8348
|
+
try {
|
|
8349
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8350
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent"), { recursive: true });
|
|
8351
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent" });
|
|
8352
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "skill-active-state.json"), {
|
|
8353
|
+
active: true,
|
|
8354
|
+
skill: "ralplan",
|
|
8355
|
+
phase: "planning",
|
|
8356
|
+
});
|
|
8357
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "ralplan-state.json"), {
|
|
8358
|
+
active: true,
|
|
8359
|
+
current_phase: "planning",
|
|
8360
|
+
});
|
|
8361
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
8362
|
+
schemaVersion: 1,
|
|
8363
|
+
sessions: {
|
|
8364
|
+
"sess-stop-skill-subagent": {
|
|
8365
|
+
session_id: "sess-stop-skill-subagent",
|
|
8366
|
+
leader_thread_id: "leader-1",
|
|
8367
|
+
updated_at: new Date().toISOString(),
|
|
8368
|
+
threads: {
|
|
8369
|
+
"leader-1": {
|
|
8370
|
+
thread_id: "leader-1",
|
|
8371
|
+
kind: "leader",
|
|
8372
|
+
first_seen_at: new Date().toISOString(),
|
|
8373
|
+
last_seen_at: new Date().toISOString(),
|
|
8374
|
+
turn_count: 1,
|
|
8375
|
+
},
|
|
8376
|
+
"sub-1": {
|
|
8377
|
+
thread_id: "sub-1",
|
|
8378
|
+
kind: "subagent",
|
|
8379
|
+
first_seen_at: new Date().toISOString(),
|
|
8380
|
+
last_seen_at: new Date().toISOString(),
|
|
8381
|
+
turn_count: 1,
|
|
8382
|
+
},
|
|
8383
|
+
},
|
|
8384
|
+
},
|
|
8385
|
+
},
|
|
8386
|
+
});
|
|
8387
|
+
|
|
8388
|
+
const result = await dispatchCodexNativeHook(
|
|
8389
|
+
{
|
|
8390
|
+
hook_event_name: "Stop",
|
|
8391
|
+
cwd,
|
|
8392
|
+
session_id: "sess-stop-skill-subagent",
|
|
8393
|
+
},
|
|
8394
|
+
{ cwd },
|
|
8395
|
+
);
|
|
8396
|
+
|
|
8397
|
+
assert.equal(result.omxEventName, "stop");
|
|
8398
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
8399
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Status: waiting/);
|
|
8400
|
+
assert.match(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
|
|
8401
|
+
assert.match(String(result.outputJson?.reason ?? ""), /then continue from the current ralplan artifact/i);
|
|
8402
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_waiting_subagent");
|
|
8403
|
+
} finally {
|
|
8404
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8405
|
+
}
|
|
8406
|
+
});
|
|
8407
|
+
|
|
8408
|
+
it("does not block on stale root ralplan skill when the explicit session-scoped canonical skill state is absent", async () => {
|
|
8409
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-"));
|
|
8410
|
+
try {
|
|
8411
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8412
|
+
await mkdir(stateDir, { recursive: true });
|
|
8413
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
8414
|
+
active: true,
|
|
8415
|
+
skill: "ralplan",
|
|
8416
|
+
phase: "planning",
|
|
8417
|
+
});
|
|
8418
|
+
|
|
7013
8419
|
const result = await dispatchCodexNativeHook(
|
|
7014
8420
|
{
|
|
7015
8421
|
hook_event_name: "Stop",
|
|
@@ -7748,16 +9154,29 @@ exit 0
|
|
|
7748
9154
|
assert.equal(result.omxEventName, "stop");
|
|
7749
9155
|
const reason = String(result.outputJson?.reason);
|
|
7750
9156
|
assert.match(reason, /Ralph completion audit is missing required evidence/);
|
|
7751
|
-
assert.match(reason, /
|
|
9157
|
+
assert.match(reason, /set "completion_audit" on the Ralph state object/);
|
|
9158
|
+
assert.doesNotMatch(reason, /state\.completion_audit/);
|
|
7752
9159
|
assert.match(reason, /repo-relative JSON file/);
|
|
7753
9160
|
assert.match(reason, /Markdown artifacts and flat top-level checklist\/evidence fields are not accepted/);
|
|
7754
9161
|
assert.equal(result.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
|
|
7755
9162
|
const reopened = JSON.parse(await readFile(statePath, "utf-8")) as Record<string, unknown>;
|
|
7756
|
-
assert.equal(reopened.active,
|
|
7757
|
-
assert.equal(reopened.current_phase, "
|
|
9163
|
+
assert.equal(reopened.active, false);
|
|
9164
|
+
assert.equal(reopened.current_phase, "complete");
|
|
7758
9165
|
assert.equal(reopened.completion_audit_gate, "blocked");
|
|
7759
9166
|
assert.equal(reopened.completion_audit_missing_reason, "missing_completion_audit");
|
|
7760
|
-
assert.equal(
|
|
9167
|
+
assert.equal(reopened.completed_at, "2026-05-10T12:00:00.000Z");
|
|
9168
|
+
|
|
9169
|
+
const repeat = await dispatchCodexNativeHook(
|
|
9170
|
+
{
|
|
9171
|
+
hook_event_name: "Stop",
|
|
9172
|
+
cwd,
|
|
9173
|
+
session_id: sessionId,
|
|
9174
|
+
last_assistant_message: "Done. Ralph complete.",
|
|
9175
|
+
},
|
|
9176
|
+
{ cwd },
|
|
9177
|
+
);
|
|
9178
|
+
assert.equal(repeat.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
|
|
9179
|
+
assert.doesNotMatch(String(repeat.outputJson?.reason), /Ralph is still active/);
|
|
7761
9180
|
} finally {
|
|
7762
9181
|
await rm(cwd, { recursive: true, force: true });
|
|
7763
9182
|
}
|
|
@@ -7985,6 +9404,47 @@ exit 0
|
|
|
7985
9404
|
}
|
|
7986
9405
|
});
|
|
7987
9406
|
|
|
9407
|
+
it("allows Stop from stale orphaned session-scoped Ralph starting iteration zero state", async () => {
|
|
9408
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-orphan-starting-ralph-"));
|
|
9409
|
+
try {
|
|
9410
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9411
|
+
const sessionId = "sess-stale-orphan-ralph";
|
|
9412
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
9413
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
|
|
9414
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
9415
|
+
active: true,
|
|
9416
|
+
mode: "ralph",
|
|
9417
|
+
current_phase: "starting",
|
|
9418
|
+
iteration: 0,
|
|
9419
|
+
session_id: sessionId,
|
|
9420
|
+
updated_at: "2000-01-01T00:00:00.000Z",
|
|
9421
|
+
});
|
|
9422
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
9423
|
+
active: true,
|
|
9424
|
+
skill: "ralph",
|
|
9425
|
+
phase: "starting",
|
|
9426
|
+
session_id: sessionId,
|
|
9427
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
|
|
9428
|
+
});
|
|
9429
|
+
|
|
9430
|
+
const result = await dispatchCodexNativeHook(
|
|
9431
|
+
{
|
|
9432
|
+
hook_event_name: "Stop",
|
|
9433
|
+
cwd,
|
|
9434
|
+
session_id: sessionId,
|
|
9435
|
+
thread_id: "thread-verifier-terminal",
|
|
9436
|
+
last_assistant_message: "APPROVE: read-only verifier evidence is fresh and sufficient.",
|
|
9437
|
+
},
|
|
9438
|
+
{ cwd },
|
|
9439
|
+
);
|
|
9440
|
+
|
|
9441
|
+
assert.equal(result.omxEventName, "stop");
|
|
9442
|
+
assert.equal(result.outputJson, null);
|
|
9443
|
+
} finally {
|
|
9444
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9445
|
+
}
|
|
9446
|
+
});
|
|
9447
|
+
|
|
7988
9448
|
it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
|
|
7989
9449
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
|
|
7990
9450
|
try {
|
|
@@ -8027,6 +9487,138 @@ exit 0
|
|
|
8027
9487
|
}
|
|
8028
9488
|
});
|
|
8029
9489
|
|
|
9490
|
+
it("retires prompt-seeded Ralph starting state when canonical Ralph already completed with audit", async () => {
|
|
9491
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-starting-"));
|
|
9492
|
+
try {
|
|
9493
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9494
|
+
const nativeSessionId = "native-hook-seed";
|
|
9495
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
9496
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
9497
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
9498
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
9499
|
+
session_id: canonicalSessionId,
|
|
9500
|
+
cwd,
|
|
9501
|
+
});
|
|
9502
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
9503
|
+
active: true,
|
|
9504
|
+
mode: "ralph",
|
|
9505
|
+
current_phase: "starting",
|
|
9506
|
+
session_id: nativeSessionId,
|
|
9507
|
+
iteration: 0,
|
|
9508
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
9509
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
9510
|
+
});
|
|
9511
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
9512
|
+
active: true,
|
|
9513
|
+
skill: "ralph",
|
|
9514
|
+
phase: "starting",
|
|
9515
|
+
session_id: nativeSessionId,
|
|
9516
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
9517
|
+
});
|
|
9518
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
9519
|
+
active: false,
|
|
9520
|
+
mode: "ralph",
|
|
9521
|
+
current_phase: "complete",
|
|
9522
|
+
session_id: canonicalSessionId,
|
|
9523
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
9524
|
+
completion_audit: {
|
|
9525
|
+
passed: true,
|
|
9526
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
9527
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
9528
|
+
},
|
|
9529
|
+
});
|
|
9530
|
+
|
|
9531
|
+
const result = await dispatchCodexNativeHook(
|
|
9532
|
+
{
|
|
9533
|
+
hook_event_name: "Stop",
|
|
9534
|
+
cwd,
|
|
9535
|
+
session_id: nativeSessionId,
|
|
9536
|
+
},
|
|
9537
|
+
{ cwd },
|
|
9538
|
+
);
|
|
9539
|
+
|
|
9540
|
+
assert.equal(result.omxEventName, "stop");
|
|
9541
|
+
assert.equal(result.outputJson, null);
|
|
9542
|
+
const retiredState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
9543
|
+
assert.equal(retiredState.active, false);
|
|
9544
|
+
assert.equal(retiredState.current_phase, "complete");
|
|
9545
|
+
assert.equal(retiredState.stop_reason, "shadowed_by_completed_canonical_ralph");
|
|
9546
|
+
assert.equal(retiredState.shadowed_by_completed_canonical_ralph.session_id, canonicalSessionId);
|
|
9547
|
+
} finally {
|
|
9548
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9549
|
+
}
|
|
9550
|
+
});
|
|
9551
|
+
|
|
9552
|
+
it("does not retire prompt-seeded Ralph starting state from a completed canonical Ralph owned by another thread", async () => {
|
|
9553
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-thread-mismatch-"));
|
|
9554
|
+
try {
|
|
9555
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9556
|
+
const nativeSessionId = "native-hook-seed";
|
|
9557
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
9558
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
9559
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
9560
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
9561
|
+
session_id: canonicalSessionId,
|
|
9562
|
+
cwd,
|
|
9563
|
+
});
|
|
9564
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
9565
|
+
active: true,
|
|
9566
|
+
mode: "ralph",
|
|
9567
|
+
current_phase: "starting",
|
|
9568
|
+
session_id: nativeSessionId,
|
|
9569
|
+
iteration: 0,
|
|
9570
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
9571
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
9572
|
+
});
|
|
9573
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
9574
|
+
active: true,
|
|
9575
|
+
skill: "ralph",
|
|
9576
|
+
phase: "starting",
|
|
9577
|
+
session_id: nativeSessionId,
|
|
9578
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
9579
|
+
});
|
|
9580
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
9581
|
+
active: false,
|
|
9582
|
+
mode: "ralph",
|
|
9583
|
+
current_phase: "complete",
|
|
9584
|
+
session_id: canonicalSessionId,
|
|
9585
|
+
owner_codex_thread_id: "thread-A",
|
|
9586
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
9587
|
+
completion_audit: {
|
|
9588
|
+
passed: true,
|
|
9589
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
9590
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
9591
|
+
},
|
|
9592
|
+
});
|
|
9593
|
+
|
|
9594
|
+
const result = await dispatchCodexNativeHook(
|
|
9595
|
+
{
|
|
9596
|
+
hook_event_name: "Stop",
|
|
9597
|
+
cwd,
|
|
9598
|
+
session_id: nativeSessionId,
|
|
9599
|
+
thread_id: "thread-B",
|
|
9600
|
+
},
|
|
9601
|
+
{ cwd },
|
|
9602
|
+
);
|
|
9603
|
+
|
|
9604
|
+
assert.equal(result.omxEventName, "stop");
|
|
9605
|
+
assert.deepEqual(result.outputJson, {
|
|
9606
|
+
decision: "block",
|
|
9607
|
+
reason:
|
|
9608
|
+
"OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
9609
|
+
stopReason: "ralph_starting",
|
|
9610
|
+
systemMessage:
|
|
9611
|
+
"OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
9612
|
+
});
|
|
9613
|
+
const preservedState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
9614
|
+
assert.equal(preservedState.active, true);
|
|
9615
|
+
assert.equal(preservedState.current_phase, "starting");
|
|
9616
|
+
assert.equal(preservedState.stop_reason, undefined);
|
|
9617
|
+
} finally {
|
|
9618
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9619
|
+
}
|
|
9620
|
+
});
|
|
9621
|
+
|
|
8030
9622
|
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
8031
9623
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
8032
9624
|
try {
|