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
|
@@ -17,6 +17,7 @@ import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
|
|
|
17
17
|
import { readAllState } from "../../hud/state.js";
|
|
18
18
|
import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
|
|
19
19
|
import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
|
|
20
|
+
import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
|
|
20
21
|
function nativeHookScriptPath() {
|
|
21
22
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
22
23
|
}
|
|
@@ -39,6 +40,38 @@ async function writeJson(path, value) {
|
|
|
39
40
|
await mkdir(dirname(path), { recursive: true }).catch(() => { });
|
|
40
41
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
41
42
|
}
|
|
43
|
+
async function withLoreGuardConfig(value, prefix, run) {
|
|
44
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-pretool-git-commit-lore-${prefix}-`));
|
|
45
|
+
const codexHome = await mkdtemp(join(tmpdir(), `omx-native-hook-codex-home-lore-${prefix}-`));
|
|
46
|
+
const defaultHome = await mkdtemp(join(tmpdir(), `omx-native-hook-home-lore-${prefix}-`));
|
|
47
|
+
const originalGuard = process.env.OMX_LORE_COMMIT_GUARD;
|
|
48
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
49
|
+
const originalHome = process.env.HOME;
|
|
50
|
+
try {
|
|
51
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
52
|
+
process.env.CODEX_HOME = codexHome;
|
|
53
|
+
process.env.HOME = defaultHome;
|
|
54
|
+
await writeFile(join(codexHome, "config.toml"), `[shell_environment_policy.set]\nOMX_LORE_COMMIT_GUARD = "${value}"\n`, "utf-8");
|
|
55
|
+
return await run(cwd);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (originalGuard === undefined)
|
|
59
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
60
|
+
else
|
|
61
|
+
process.env.OMX_LORE_COMMIT_GUARD = originalGuard;
|
|
62
|
+
if (originalCodexHome === undefined)
|
|
63
|
+
delete process.env.CODEX_HOME;
|
|
64
|
+
else
|
|
65
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
66
|
+
if (originalHome === undefined)
|
|
67
|
+
delete process.env.HOME;
|
|
68
|
+
else
|
|
69
|
+
process.env.HOME = originalHome;
|
|
70
|
+
await rm(cwd, { recursive: true, force: true });
|
|
71
|
+
await rm(codexHome, { recursive: true, force: true });
|
|
72
|
+
await rm(defaultHome, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
42
75
|
function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
|
|
43
76
|
return `#!/usr/bin/env bash
|
|
44
77
|
set -eu
|
|
@@ -67,7 +100,7 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
67
100
|
exit 0
|
|
68
101
|
fi
|
|
69
102
|
if [[ "$cmd" == "capture-pane" ]]; then
|
|
70
|
-
echo "› ready"
|
|
103
|
+
${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
|
|
71
104
|
exit 0
|
|
72
105
|
fi
|
|
73
106
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
@@ -567,7 +600,16 @@ describe("codex native hook dispatch", () => {
|
|
|
567
600
|
});
|
|
568
601
|
it("keeps subagent SessionStart from replacing the canonical leader session", async () => {
|
|
569
602
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-"));
|
|
570
|
-
|
|
603
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
604
|
+
try {
|
|
605
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
606
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
607
|
+
notifications: {
|
|
608
|
+
enabled: true,
|
|
609
|
+
verbosity: "session",
|
|
610
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
611
|
+
},
|
|
612
|
+
});
|
|
571
613
|
const stateDir = join(cwd, ".omx", "state");
|
|
572
614
|
const canonicalSessionId = "omx-leader-session";
|
|
573
615
|
const leaderNativeSessionId = "codex-leader-thread";
|
|
@@ -583,6 +625,13 @@ describe("codex native hook dispatch", () => {
|
|
|
583
625
|
iteration: 1,
|
|
584
626
|
max_iterations: 5,
|
|
585
627
|
});
|
|
628
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
629
|
+
await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
|
|
630
|
+
"import { appendFileSync } from 'node:fs';",
|
|
631
|
+
"export async function onHookEvent(event) {",
|
|
632
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event, context: event.context })}\\n`);",
|
|
633
|
+
"}",
|
|
634
|
+
].join("\n"));
|
|
586
635
|
const transcriptPath = join(cwd, "subagent-rollout.jsonl");
|
|
587
636
|
await writeFile(transcriptPath, `${JSON.stringify({
|
|
588
637
|
type: "session_meta",
|
|
@@ -616,6 +665,7 @@ describe("codex native hook dispatch", () => {
|
|
|
616
665
|
const leaderRalph = JSON.parse(await readFile(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), "utf-8"));
|
|
617
666
|
assert.equal(leaderRalph.active, true);
|
|
618
667
|
assert.equal(leaderRalph.current_phase, "executing");
|
|
668
|
+
assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent SessionStart must not independently dispatch session-start hook notifications");
|
|
619
669
|
const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
|
|
620
670
|
assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, leaderNativeSessionId);
|
|
621
671
|
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
|
|
@@ -623,8 +673,213 @@ describe("codex native hook dispatch", () => {
|
|
|
623
673
|
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
|
|
624
674
|
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
|
|
625
675
|
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
|
|
676
|
+
await dispatchCodexNativeHook({
|
|
677
|
+
hook_event_name: "Stop",
|
|
678
|
+
cwd,
|
|
679
|
+
session_id: childNativeSessionId,
|
|
680
|
+
thread_id: childNativeSessionId,
|
|
681
|
+
turn_id: "child-stop-turn",
|
|
682
|
+
}, { cwd });
|
|
683
|
+
assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent Stop must not independently dispatch stop hook notifications");
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
if (originalCodexHome === undefined) {
|
|
687
|
+
delete process.env.CODEX_HOME;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
691
|
+
}
|
|
692
|
+
await rm(cwd, { recursive: true, force: true });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
it("suppresses child-agent SessionStart hook dispatch at minimal verbosity", async () => {
|
|
696
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-minimal-"));
|
|
697
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
698
|
+
try {
|
|
699
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
700
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
701
|
+
notifications: {
|
|
702
|
+
enabled: true,
|
|
703
|
+
verbosity: "minimal",
|
|
704
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
708
|
+
const canonicalSessionId = "omx-leader-session-minimal";
|
|
709
|
+
const leaderNativeSessionId = "codex-leader-thread-minimal";
|
|
710
|
+
const childNativeSessionId = "codex-child-thread-minimal";
|
|
711
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
712
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
713
|
+
nativeSessionId: leaderNativeSessionId,
|
|
714
|
+
});
|
|
715
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
716
|
+
await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
|
|
717
|
+
"import { appendFileSync } from 'node:fs';",
|
|
718
|
+
"export async function onHookEvent(event) {",
|
|
719
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
720
|
+
"}",
|
|
721
|
+
].join("\n"));
|
|
722
|
+
const transcriptPath = join(cwd, "minimal-subagent-rollout.jsonl");
|
|
723
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
724
|
+
type: "session_meta",
|
|
725
|
+
payload: {
|
|
726
|
+
id: childNativeSessionId,
|
|
727
|
+
source: {
|
|
728
|
+
subagent: {
|
|
729
|
+
thread_spawn: {
|
|
730
|
+
parent_thread_id: leaderNativeSessionId,
|
|
731
|
+
agent_role: "verifier",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
})}\n`);
|
|
737
|
+
await dispatchCodexNativeHook({
|
|
738
|
+
hook_event_name: "SessionStart",
|
|
739
|
+
cwd,
|
|
740
|
+
session_id: childNativeSessionId,
|
|
741
|
+
transcript_path: transcriptPath,
|
|
742
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
743
|
+
assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent SessionStart must be suppressed at minimal verbosity");
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
if (originalCodexHome === undefined) {
|
|
747
|
+
delete process.env.CODEX_HOME;
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
751
|
+
}
|
|
752
|
+
await rm(cwd, { recursive: true, force: true });
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
it("allows explicit child-agent lifecycle hook dispatch when includeChildAgents is enabled", async () => {
|
|
756
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-include-"));
|
|
757
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
758
|
+
try {
|
|
759
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
760
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
761
|
+
notifications: {
|
|
762
|
+
enabled: true,
|
|
763
|
+
verbosity: "session",
|
|
764
|
+
includeChildAgents: true,
|
|
765
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
769
|
+
const canonicalSessionId = "omx-leader-session-include";
|
|
770
|
+
const leaderNativeSessionId = "codex-leader-thread-include";
|
|
771
|
+
const childNativeSessionId = "codex-child-thread-include";
|
|
772
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
773
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
774
|
+
nativeSessionId: leaderNativeSessionId,
|
|
775
|
+
});
|
|
776
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
777
|
+
await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
|
|
778
|
+
"import { appendFileSync } from 'node:fs';",
|
|
779
|
+
"export async function onHookEvent(event) {",
|
|
780
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
781
|
+
"}",
|
|
782
|
+
].join("\n"));
|
|
783
|
+
const transcriptPath = join(cwd, "included-subagent-rollout.jsonl");
|
|
784
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
785
|
+
type: "session_meta",
|
|
786
|
+
payload: {
|
|
787
|
+
id: childNativeSessionId,
|
|
788
|
+
source: {
|
|
789
|
+
subagent: {
|
|
790
|
+
thread_spawn: {
|
|
791
|
+
parent_thread_id: leaderNativeSessionId,
|
|
792
|
+
agent_role: "verifier",
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
})}\n`);
|
|
798
|
+
await dispatchCodexNativeHook({
|
|
799
|
+
hook_event_name: "SessionStart",
|
|
800
|
+
cwd,
|
|
801
|
+
session_id: childNativeSessionId,
|
|
802
|
+
transcript_path: transcriptPath,
|
|
803
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
804
|
+
await dispatchCodexNativeHook({
|
|
805
|
+
hook_event_name: "Stop",
|
|
806
|
+
cwd,
|
|
807
|
+
session_id: childNativeSessionId,
|
|
808
|
+
thread_id: childNativeSessionId,
|
|
809
|
+
turn_id: "included-child-stop-turn",
|
|
810
|
+
}, { cwd });
|
|
811
|
+
const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
|
|
812
|
+
assert.match(hookEvents, /"event":"session-start"/);
|
|
813
|
+
assert.match(hookEvents, /"event":"stop"/);
|
|
814
|
+
}
|
|
815
|
+
finally {
|
|
816
|
+
if (originalCodexHome === undefined) {
|
|
817
|
+
delete process.env.CODEX_HOME;
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
821
|
+
}
|
|
822
|
+
await rm(cwd, { recursive: true, force: true });
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
it("allows child-agent lifecycle hook dispatch at agent verbosity", async () => {
|
|
826
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-agent-"));
|
|
827
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
828
|
+
try {
|
|
829
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
830
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
831
|
+
notifications: {
|
|
832
|
+
enabled: true,
|
|
833
|
+
verbosity: "agent",
|
|
834
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
838
|
+
const canonicalSessionId = "omx-leader-session-agent";
|
|
839
|
+
const leaderNativeSessionId = "codex-leader-thread-agent";
|
|
840
|
+
const childNativeSessionId = "codex-child-thread-agent";
|
|
841
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
842
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
843
|
+
nativeSessionId: leaderNativeSessionId,
|
|
844
|
+
});
|
|
845
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
846
|
+
await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
|
|
847
|
+
"import { appendFileSync } from 'node:fs';",
|
|
848
|
+
"export async function onHookEvent(event) {",
|
|
849
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
850
|
+
"}",
|
|
851
|
+
].join("\n"));
|
|
852
|
+
const transcriptPath = join(cwd, "agent-verbosity-subagent-rollout.jsonl");
|
|
853
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
854
|
+
type: "session_meta",
|
|
855
|
+
payload: {
|
|
856
|
+
id: childNativeSessionId,
|
|
857
|
+
source: {
|
|
858
|
+
subagent: {
|
|
859
|
+
thread_spawn: {
|
|
860
|
+
parent_thread_id: leaderNativeSessionId,
|
|
861
|
+
agent_role: "verifier",
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
})}\n`);
|
|
867
|
+
await dispatchCodexNativeHook({
|
|
868
|
+
hook_event_name: "SessionStart",
|
|
869
|
+
cwd,
|
|
870
|
+
session_id: childNativeSessionId,
|
|
871
|
+
transcript_path: transcriptPath,
|
|
872
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
873
|
+
const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
|
|
874
|
+
assert.match(hookEvents, /"event":"session-start"/);
|
|
626
875
|
}
|
|
627
876
|
finally {
|
|
877
|
+
if (originalCodexHome === undefined) {
|
|
878
|
+
delete process.env.CODEX_HOME;
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
882
|
+
}
|
|
628
883
|
await rm(cwd, { recursive: true, force: true });
|
|
629
884
|
}
|
|
630
885
|
});
|
|
@@ -1218,6 +1473,62 @@ describe("codex native hook dispatch", () => {
|
|
|
1218
1473
|
await rm(cwd, { recursive: true, force: true });
|
|
1219
1474
|
}
|
|
1220
1475
|
});
|
|
1476
|
+
it("does not repeat performance-goal reconciliation after a recorded objective mismatch blocker", async () => {
|
|
1477
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-mismatch-blocked-stop-"));
|
|
1478
|
+
try {
|
|
1479
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
1480
|
+
version: 1,
|
|
1481
|
+
workflow: "performance-goal",
|
|
1482
|
+
slug: "latency",
|
|
1483
|
+
objective: "Reduce latency",
|
|
1484
|
+
status: "blocked",
|
|
1485
|
+
lastValidation: {
|
|
1486
|
+
status: "blocked",
|
|
1487
|
+
evidence: "omx performance-goal complete rejected the fresh get_goal snapshot: Codex goal objective mismatch: expected \"reduce latency\", got \"legacy objective\".",
|
|
1488
|
+
recordedAt: "2026-05-20T00:00:00.000Z",
|
|
1489
|
+
},
|
|
1490
|
+
});
|
|
1491
|
+
const result = await dispatchCodexNativeHook({
|
|
1492
|
+
hook_event_name: "Stop",
|
|
1493
|
+
cwd,
|
|
1494
|
+
session_id: "sess-performance-mismatch-blocked-stop",
|
|
1495
|
+
thread_id: "thread-performance-mismatch-blocked-stop",
|
|
1496
|
+
last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
|
|
1497
|
+
}, { cwd });
|
|
1498
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
1499
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
|
|
1500
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1501
|
+
}
|
|
1502
|
+
finally {
|
|
1503
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
it("does not block Stop for an already complete performance-goal state", async () => {
|
|
1507
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-complete-stop-"));
|
|
1508
|
+
try {
|
|
1509
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
1510
|
+
version: 1,
|
|
1511
|
+
workflow: "performance-goal",
|
|
1512
|
+
slug: "latency",
|
|
1513
|
+
objective: "Reduce latency",
|
|
1514
|
+
status: "complete",
|
|
1515
|
+
completedAt: "2026-05-20T00:00:00.000Z",
|
|
1516
|
+
});
|
|
1517
|
+
const result = await dispatchCodexNativeHook({
|
|
1518
|
+
hook_event_name: "Stop",
|
|
1519
|
+
cwd,
|
|
1520
|
+
session_id: "sess-performance-complete-stop",
|
|
1521
|
+
thread_id: "thread-performance-complete-stop",
|
|
1522
|
+
last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
|
|
1523
|
+
}, { cwd });
|
|
1524
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
1525
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
|
|
1526
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1527
|
+
}
|
|
1528
|
+
finally {
|
|
1529
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1221
1532
|
it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
|
|
1222
1533
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
|
|
1223
1534
|
try {
|
|
@@ -1262,7 +1573,7 @@ describe("codex native hook dispatch", () => {
|
|
|
1262
1573
|
await rm(cwd, { recursive: true, force: true });
|
|
1263
1574
|
}
|
|
1264
1575
|
});
|
|
1265
|
-
it("blocks ultragoal Stop with blocked checkpoint and
|
|
1576
|
+
it("blocks ultragoal Stop with blocked checkpoint and available-goal-context remediation for completed legacy snapshots", async () => {
|
|
1266
1577
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
|
|
1267
1578
|
try {
|
|
1268
1579
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
@@ -1281,7 +1592,8 @@ describe("codex native hook dispatch", () => {
|
|
|
1281
1592
|
assert.equal(result.outputJson?.decision, "block");
|
|
1282
1593
|
assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
1283
1594
|
assert.match(output, /--status blocked/);
|
|
1284
|
-
assert.match(output, /
|
|
1595
|
+
assert.match(output, /Codex goal context/);
|
|
1596
|
+
assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
|
|
1285
1597
|
assert.match(output, /Hooks must not mutate Codex goal state/);
|
|
1286
1598
|
}
|
|
1287
1599
|
finally {
|
|
@@ -1294,7 +1606,7 @@ describe("codex native hook dispatch", () => {
|
|
|
1294
1606
|
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
1295
1607
|
version: 1,
|
|
1296
1608
|
codexGoalMode: "aggregate",
|
|
1297
|
-
codexObjective: "Complete
|
|
1609
|
+
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.",
|
|
1298
1610
|
activeGoalId: "G001-micro",
|
|
1299
1611
|
aggregateCompletion: {
|
|
1300
1612
|
status: "complete",
|
|
@@ -1409,6 +1721,67 @@ describe("codex native hook dispatch", () => {
|
|
|
1409
1721
|
await rm(cwd, { recursive: true, force: true });
|
|
1410
1722
|
}
|
|
1411
1723
|
});
|
|
1724
|
+
it("does not repeat Stop block when the last autoresearch-goal completion attempt reported objective mismatch", async () => {
|
|
1725
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-reported-stop-"));
|
|
1726
|
+
try {
|
|
1727
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "mission.json"), {
|
|
1728
|
+
version: 1,
|
|
1729
|
+
workflow: "autoresearch-goal",
|
|
1730
|
+
slug: "mismatched-mission",
|
|
1731
|
+
topic: "Passing research bound to another Codex goal",
|
|
1732
|
+
status: "passed",
|
|
1733
|
+
});
|
|
1734
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "completion.json"), {
|
|
1735
|
+
verdict: "pass",
|
|
1736
|
+
passed: true,
|
|
1737
|
+
});
|
|
1738
|
+
const result = await dispatchCodexNativeHook({
|
|
1739
|
+
hook_event_name: "Stop",
|
|
1740
|
+
cwd,
|
|
1741
|
+
session_id: "sess-autoresearch-mismatch-reported-stop",
|
|
1742
|
+
thread_id: "thread-autoresearch-mismatch-reported-stop",
|
|
1743
|
+
last_assistant_message: [
|
|
1744
|
+
"I called get_goal and ran omx autoresearch-goal complete --slug mismatched-mission --codex-goal-json /tmp/snapshot.json.",
|
|
1745
|
+
"The autoresearch-goal completion failed with Codex goal objective mismatch, so I will not repeat the same complete command blindly in this thread.",
|
|
1746
|
+
].join("\n"),
|
|
1747
|
+
}, { cwd });
|
|
1748
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
1749
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /autoresearch-goal complete --slug mismatched-mission/);
|
|
1750
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1751
|
+
}
|
|
1752
|
+
finally {
|
|
1753
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
it("still blocks later autoresearch-goal completion claims after an objective mismatch if no mismatch is reported in the final answer", async () => {
|
|
1757
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-later-retry-stop-"));
|
|
1758
|
+
try {
|
|
1759
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "mission.json"), {
|
|
1760
|
+
version: 1,
|
|
1761
|
+
workflow: "autoresearch-goal",
|
|
1762
|
+
slug: "retryable-mission",
|
|
1763
|
+
topic: "Passing research that can still retry with the correct snapshot",
|
|
1764
|
+
status: "passed",
|
|
1765
|
+
});
|
|
1766
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "completion.json"), {
|
|
1767
|
+
verdict: "pass",
|
|
1768
|
+
passed: true,
|
|
1769
|
+
});
|
|
1770
|
+
const result = await dispatchCodexNativeHook({
|
|
1771
|
+
hook_event_name: "Stop",
|
|
1772
|
+
cwd,
|
|
1773
|
+
session_id: "sess-autoresearch-mismatch-later-retry-stop",
|
|
1774
|
+
thread_id: "thread-autoresearch-mismatch-later-retry-stop",
|
|
1775
|
+
last_assistant_message: "Autoresearch goal complete; next call update_goal({status: \"complete\"}).",
|
|
1776
|
+
}, { cwd });
|
|
1777
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
1778
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1779
|
+
assert.match(JSON.stringify(result.outputJson), /omx autoresearch-goal complete --slug retryable-mission/);
|
|
1780
|
+
}
|
|
1781
|
+
finally {
|
|
1782
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1412
1785
|
it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
|
|
1413
1786
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
|
|
1414
1787
|
try {
|
|
@@ -1512,49 +1885,205 @@ describe("codex native hook dispatch", () => {
|
|
|
1512
1885
|
assert.match(message, /get_goal/);
|
|
1513
1886
|
assert.match(message, /create_goal/);
|
|
1514
1887
|
assert.match(message, /update_goal/);
|
|
1888
|
+
assert.match(message, /does not call `\/goal clear`/);
|
|
1889
|
+
assert.match(message, /multiple sequential ultragoal runs/);
|
|
1515
1890
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
|
|
1516
1891
|
}
|
|
1517
1892
|
finally {
|
|
1518
1893
|
await rm(cwd, { recursive: true, force: true });
|
|
1519
1894
|
}
|
|
1520
1895
|
});
|
|
1521
|
-
it("
|
|
1522
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
1896
|
+
it("applies only explicit structured UserPromptSubmit ultragoal steering directives", async () => {
|
|
1897
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-"));
|
|
1523
1898
|
try {
|
|
1524
|
-
await
|
|
1899
|
+
await createUltragoalPlan(cwd, {
|
|
1900
|
+
brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal hook steering fixture",
|
|
1901
|
+
goals: [{ title: "First", objective: "Complete first milestone with tests." }],
|
|
1902
|
+
});
|
|
1903
|
+
const prose = await dispatchCodexNativeHook({
|
|
1904
|
+
hook_event_name: "UserPromptSubmit",
|
|
1905
|
+
cwd,
|
|
1906
|
+
session_id: "sess-ultragoal-steer-1",
|
|
1907
|
+
prompt: "Please add a subgoal for docs later; this is normal prose, not a directive.",
|
|
1908
|
+
}, { cwd });
|
|
1909
|
+
assert.equal(prose.outputJson, null);
|
|
1910
|
+
assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
|
|
1911
|
+
const jsonExample = await dispatchCodexNativeHook({
|
|
1912
|
+
hook_event_name: "UserPromptSubmit",
|
|
1913
|
+
cwd,
|
|
1914
|
+
session_id: "sess-ultragoal-steer-1",
|
|
1915
|
+
prompt: `Here is an inert example:\n\`\`\`json\n${JSON.stringify({
|
|
1916
|
+
kind: "add_subgoal",
|
|
1917
|
+
source: "user_prompt_submit",
|
|
1918
|
+
evidence: "Example JSON should not mutate .omx/ultragoal.",
|
|
1919
|
+
rationale: "Only explicit steering fences or labels are executable.",
|
|
1920
|
+
title: "Inert JSON example",
|
|
1921
|
+
objective: "This example must not be added.",
|
|
1922
|
+
})}\n\`\`\``,
|
|
1923
|
+
}, { cwd });
|
|
1924
|
+
assert.equal(jsonExample.outputJson, null);
|
|
1925
|
+
assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
|
|
1525
1926
|
const result = await dispatchCodexNativeHook({
|
|
1526
1927
|
hook_event_name: "UserPromptSubmit",
|
|
1527
1928
|
cwd,
|
|
1528
|
-
session_id: "sess-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1929
|
+
session_id: "sess-ultragoal-steer-1",
|
|
1930
|
+
prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
|
|
1931
|
+
kind: "add_subgoal",
|
|
1932
|
+
source: "user_prompt_submit",
|
|
1933
|
+
evidence: "Prompt-submit supplied a structured .omx/ultragoal directive for G002-cli-and-prompt-submit-bridge.",
|
|
1934
|
+
rationale: "Add bounded hook regression work while preserving all completion gates.",
|
|
1935
|
+
title: "Prompt bridge regression",
|
|
1936
|
+
objective: "Verify UserPromptSubmit bounded steering bridge with tests.",
|
|
1937
|
+
})}`,
|
|
1532
1938
|
}, { cwd });
|
|
1533
|
-
|
|
1534
|
-
assert.
|
|
1535
|
-
assert.
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
assert.equal(
|
|
1939
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
1940
|
+
assert.match(message, /bounded \.omx\/ultragoal steering/);
|
|
1941
|
+
assert.match(message, /G002-cli-and-prompt-submit-bridge/);
|
|
1942
|
+
assert.match(message, /accepted/);
|
|
1943
|
+
const plan = await readUltragoalPlan(cwd);
|
|
1944
|
+
assert.equal(plan.goals.length, 2);
|
|
1945
|
+
assert.equal(plan.goals[1]?.title, "Prompt bridge regression");
|
|
1539
1946
|
}
|
|
1540
1947
|
finally {
|
|
1541
1948
|
await rm(cwd, { recursive: true, force: true });
|
|
1542
1949
|
}
|
|
1543
1950
|
});
|
|
1544
|
-
it("
|
|
1545
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
1951
|
+
it("does not apply UserPromptSubmit ultragoal steering from native subagent prompts", async () => {
|
|
1952
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-subagent-"));
|
|
1546
1953
|
try {
|
|
1547
|
-
await
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1954
|
+
await createUltragoalPlan(cwd, {
|
|
1955
|
+
brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal subagent steering fixture",
|
|
1956
|
+
goals: [{ title: "First", objective: "Complete first milestone with tests." }],
|
|
1957
|
+
});
|
|
1958
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1959
|
+
const canonicalSessionId = "sess-ultragoal-parent";
|
|
1960
|
+
const leaderNativeSessionId = "native-ultragoal-parent";
|
|
1961
|
+
const childNativeSessionId = "native-ultragoal-child";
|
|
1962
|
+
const nowIso = new Date().toISOString();
|
|
1963
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
1964
|
+
session_id: canonicalSessionId,
|
|
1965
|
+
native_session_id: leaderNativeSessionId,
|
|
1966
|
+
});
|
|
1967
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
1968
|
+
schemaVersion: 1,
|
|
1969
|
+
sessions: {
|
|
1970
|
+
[canonicalSessionId]: {
|
|
1971
|
+
session_id: canonicalSessionId,
|
|
1972
|
+
leader_thread_id: leaderNativeSessionId,
|
|
1973
|
+
updated_at: nowIso,
|
|
1974
|
+
threads: {
|
|
1975
|
+
[leaderNativeSessionId]: {
|
|
1976
|
+
thread_id: leaderNativeSessionId,
|
|
1977
|
+
kind: "leader",
|
|
1978
|
+
first_seen_at: nowIso,
|
|
1979
|
+
last_seen_at: nowIso,
|
|
1980
|
+
turn_count: 1,
|
|
1981
|
+
},
|
|
1982
|
+
[childNativeSessionId]: {
|
|
1983
|
+
thread_id: childNativeSessionId,
|
|
1984
|
+
kind: "subagent",
|
|
1985
|
+
first_seen_at: nowIso,
|
|
1986
|
+
last_seen_at: nowIso,
|
|
1987
|
+
turn_count: 1,
|
|
1988
|
+
mode: "architect",
|
|
1989
|
+
},
|
|
1990
|
+
},
|
|
1991
|
+
},
|
|
1992
|
+
},
|
|
1993
|
+
});
|
|
1994
|
+
const result = await dispatchCodexNativeHook({
|
|
1995
|
+
hook_event_name: "UserPromptSubmit",
|
|
1996
|
+
cwd,
|
|
1997
|
+
session_id: childNativeSessionId,
|
|
1998
|
+
thread_id: childNativeSessionId,
|
|
1999
|
+
turn_id: "turn-ultragoal-child-1",
|
|
2000
|
+
prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
|
|
2001
|
+
kind: "add_subgoal",
|
|
2002
|
+
source: "user_prompt_submit",
|
|
2003
|
+
evidence: "Subagent prompt text must be literal delegated context.",
|
|
2004
|
+
rationale: "Subagent prompts should not mutate the parent .omx/ultragoal ledger.",
|
|
2005
|
+
title: "Subagent should not add this",
|
|
2006
|
+
objective: "This must remain literal prompt text.",
|
|
2007
|
+
})}`,
|
|
2008
|
+
}, { cwd });
|
|
2009
|
+
assert.equal(result.outputJson, null);
|
|
2010
|
+
const plan = await readUltragoalPlan(cwd);
|
|
2011
|
+
assert.equal(plan.goals.length, 1);
|
|
2012
|
+
const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
|
|
2013
|
+
assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 0);
|
|
2014
|
+
assert.equal((ledger.match(/"event":"steering_rejected"/g) ?? []).length, 0);
|
|
2015
|
+
}
|
|
2016
|
+
finally {
|
|
2017
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
it("dedupes repeated UserPromptSubmit ultragoal steering directives by prompt signature", async () => {
|
|
2021
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-dedupe-"));
|
|
2022
|
+
try {
|
|
2023
|
+
await createUltragoalPlan(cwd, {
|
|
2024
|
+
brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal dedupe fixture",
|
|
2025
|
+
goals: [{ title: "First", objective: "Complete first milestone with tests." }],
|
|
2026
|
+
});
|
|
2027
|
+
const prompt = `\`\`\`omx-ultragoal-steer
|
|
2028
|
+
${JSON.stringify({
|
|
2029
|
+
kind: "add_subgoal",
|
|
2030
|
+
source: "user_prompt_submit",
|
|
2031
|
+
evidence: "Structured prompt-submit directive adds exactly one deduped goal.",
|
|
2032
|
+
rationale: "Use idempotent bridge semantics for repeated hook delivery.",
|
|
2033
|
+
title: "Deduped bridge regression",
|
|
2034
|
+
objective: "Verify repeated UserPromptSubmit steering does not duplicate goals.",
|
|
2035
|
+
})}
|
|
2036
|
+
\`\`\``;
|
|
2037
|
+
await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
|
|
2038
|
+
const second = await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
|
|
2039
|
+
const message = String(second.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2040
|
+
assert.match(message, /deduped/);
|
|
2041
|
+
const plan = await readUltragoalPlan(cwd);
|
|
2042
|
+
assert.equal(plan.goals.filter((goal) => goal.title === "Deduped bridge regression").length, 1);
|
|
2043
|
+
const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
|
|
2044
|
+
assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 1);
|
|
2045
|
+
}
|
|
2046
|
+
finally {
|
|
2047
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
|
|
2051
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
|
|
2052
|
+
try {
|
|
2053
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2054
|
+
const result = await dispatchCodexNativeHook({
|
|
2055
|
+
hook_event_name: "UserPromptSubmit",
|
|
2056
|
+
cwd,
|
|
2057
|
+
session_id: "sess-ulw-ko",
|
|
2058
|
+
thread_id: "thread-ulw-ko",
|
|
2059
|
+
turn_id: "turn-ulw-ko",
|
|
2060
|
+
prompt: "ㅕㅣㅈ로 병렬 처리해줘",
|
|
2061
|
+
}, { cwd });
|
|
2062
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2063
|
+
assert.equal(result.skillState?.skill, "ultrawork");
|
|
2064
|
+
assert.equal(result.skillState?.keyword, "ulw");
|
|
2065
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2066
|
+
assert.match(additionalContext, /workflow keyword \"ulw\" -> ultrawork/);
|
|
2067
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ulw-ko", "ultrawork-state.json")), true);
|
|
2068
|
+
}
|
|
2069
|
+
finally {
|
|
2070
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
it("adds ultrawork-specific activation guidance only for true ultrawork workflow activation", async () => {
|
|
2074
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultrawork-routing-"));
|
|
2075
|
+
try {
|
|
2076
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2077
|
+
const result = await dispatchCodexNativeHook({
|
|
2078
|
+
hook_event_name: "UserPromptSubmit",
|
|
2079
|
+
cwd,
|
|
2080
|
+
session_id: "sess-ultrawork-msg",
|
|
2081
|
+
thread_id: "thread-ultrawork-msg",
|
|
2082
|
+
turn_id: "turn-ultrawork-msg",
|
|
2083
|
+
prompt: "$ultrawork fan out the regression checks",
|
|
2084
|
+
}, { cwd });
|
|
2085
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2086
|
+
assert.equal(result.skillState?.skill, "ultrawork");
|
|
1558
2087
|
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
1559
2088
|
assert.match(message, /\$ultrawork" -> ultrawork/);
|
|
1560
2089
|
assert.match(message, /ground the task before editing/i);
|
|
@@ -2370,6 +2899,44 @@ exit 0
|
|
|
2370
2899
|
await rm(cwd, { recursive: true, force: true });
|
|
2371
2900
|
}
|
|
2372
2901
|
});
|
|
2902
|
+
it("does not block Bash commands that only mention omx question in quoted arguments", async () => {
|
|
2903
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-quoted-mention-"));
|
|
2904
|
+
try {
|
|
2905
|
+
const result = await dispatchCodexNativeHook({
|
|
2906
|
+
hook_event_name: "PreToolUse",
|
|
2907
|
+
cwd,
|
|
2908
|
+
tool_name: "Bash",
|
|
2909
|
+
tool_use_id: "tool-question-quoted-mention",
|
|
2910
|
+
tool_input: {
|
|
2911
|
+
command: `omx ultragoal create-goals --brief "Deep interview says omx question failed in tmux"`,
|
|
2912
|
+
},
|
|
2913
|
+
}, { cwd });
|
|
2914
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2915
|
+
assert.equal(result.outputJson, null);
|
|
2916
|
+
}
|
|
2917
|
+
finally {
|
|
2918
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
it("does not block Bash heredocs that only document omx question text", async () => {
|
|
2922
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-heredoc-mention-"));
|
|
2923
|
+
try {
|
|
2924
|
+
const result = await dispatchCodexNativeHook({
|
|
2925
|
+
hook_event_name: "PreToolUse",
|
|
2926
|
+
cwd,
|
|
2927
|
+
tool_name: "Bash",
|
|
2928
|
+
tool_use_id: "tool-question-heredoc-mention",
|
|
2929
|
+
tool_input: {
|
|
2930
|
+
command: `cat > issue-notes.md <<'EOF'\nomx question failed in the attached tmux pane\nEOF`,
|
|
2931
|
+
},
|
|
2932
|
+
}, { cwd });
|
|
2933
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2934
|
+
assert.equal(result.outputJson, null);
|
|
2935
|
+
}
|
|
2936
|
+
finally {
|
|
2937
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
2373
2940
|
it("allows Bash omx question when the command preserves the leader-pane return hint", async () => {
|
|
2374
2941
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-allow-"));
|
|
2375
2942
|
try {
|
|
@@ -3010,7 +3577,7 @@ exit 0
|
|
|
3010
3577
|
cwd,
|
|
3011
3578
|
tool_name: "Bash",
|
|
3012
3579
|
tool_use_id: "tool-slop-git-priority",
|
|
3013
|
-
tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
|
|
3580
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "quick hack fallback if it fails"' },
|
|
3014
3581
|
}, { cwd });
|
|
3015
3582
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3016
3583
|
assert.equal(result.outputJson?.decision, "block");
|
|
@@ -3029,7 +3596,7 @@ exit 0
|
|
|
3029
3596
|
cwd,
|
|
3030
3597
|
tool_name: "Bash",
|
|
3031
3598
|
tool_use_id: "tool-git-commit-invalid",
|
|
3032
|
-
tool_input: { command: 'git commit -m "fix tests"' },
|
|
3599
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
|
|
3033
3600
|
}, { cwd });
|
|
3034
3601
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3035
3602
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3054,11 +3621,35 @@ exit 0
|
|
|
3054
3621
|
await rm(cwd, { recursive: true, force: true });
|
|
3055
3622
|
}
|
|
3056
3623
|
});
|
|
3057
|
-
it("
|
|
3624
|
+
it("blocks PreToolUse git commit when process env explicitly enables the Lore commit guard", async () => {
|
|
3625
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-enabled-"));
|
|
3626
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3627
|
+
try {
|
|
3628
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
3629
|
+
const result = await dispatchCodexNativeHook({
|
|
3630
|
+
hook_event_name: "PreToolUse",
|
|
3631
|
+
cwd,
|
|
3632
|
+
tool_name: "Bash",
|
|
3633
|
+
tool_use_id: "tool-git-commit-lore-env-enabled",
|
|
3634
|
+
tool_input: { command: 'git commit -m "fix tests"' },
|
|
3635
|
+
}, { cwd });
|
|
3636
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3637
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
3638
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3639
|
+
}
|
|
3640
|
+
finally {
|
|
3641
|
+
if (original === undefined)
|
|
3642
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3643
|
+
else
|
|
3644
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3645
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3646
|
+
}
|
|
3647
|
+
});
|
|
3648
|
+
it("allows non-Lore git commit messages when the Lore commit guard is disabled by default", async () => {
|
|
3058
3649
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
|
|
3059
3650
|
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3060
3651
|
try {
|
|
3061
|
-
process.env.OMX_LORE_COMMIT_GUARD
|
|
3652
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3062
3653
|
const result = await dispatchCodexNativeHook({
|
|
3063
3654
|
hook_event_name: "PreToolUse",
|
|
3064
3655
|
cwd,
|
|
@@ -3077,6 +3668,60 @@ exit 0
|
|
|
3077
3668
|
await rm(cwd, { recursive: true, force: true });
|
|
3078
3669
|
}
|
|
3079
3670
|
});
|
|
3671
|
+
it("blocks non-Lore git commit messages when the Lore commit guard is enabled in CODEX_HOME config.toml", async () => {
|
|
3672
|
+
await withLoreGuardConfig("1", "config-enabled", async (cwd) => {
|
|
3673
|
+
const result = await dispatchCodexNativeHook({
|
|
3674
|
+
hook_event_name: "PreToolUse",
|
|
3675
|
+
cwd,
|
|
3676
|
+
tool_name: "Bash",
|
|
3677
|
+
tool_use_id: "tool-git-commit-lore-config-enabled",
|
|
3678
|
+
tool_input: { command: 'git commit -m "fix: conventional"' },
|
|
3679
|
+
}, { cwd });
|
|
3680
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3681
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
3682
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3683
|
+
});
|
|
3684
|
+
});
|
|
3685
|
+
it("allows non-Lore git commit messages when the Lore commit guard is disabled in CODEX_HOME config.toml", async () => {
|
|
3686
|
+
await withLoreGuardConfig("0", "config-disabled", async (cwd) => {
|
|
3687
|
+
const result = await dispatchCodexNativeHook({
|
|
3688
|
+
hook_event_name: "PreToolUse",
|
|
3689
|
+
cwd,
|
|
3690
|
+
tool_name: "Bash",
|
|
3691
|
+
tool_use_id: "tool-git-commit-lore-config-disabled",
|
|
3692
|
+
tool_input: { command: 'git commit -m "fix: use conventional commit"' },
|
|
3693
|
+
}, { cwd });
|
|
3694
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3695
|
+
assert.equal(result.outputJson, null);
|
|
3696
|
+
});
|
|
3697
|
+
});
|
|
3698
|
+
it("lets inline Lore commit guard values override a disabled CODEX_HOME config.toml", async () => {
|
|
3699
|
+
await withLoreGuardConfig("0", "config-inline-enabled", async (cwd) => {
|
|
3700
|
+
const result = await dispatchCodexNativeHook({
|
|
3701
|
+
hook_event_name: "PreToolUse",
|
|
3702
|
+
cwd,
|
|
3703
|
+
tool_name: "Bash",
|
|
3704
|
+
tool_use_id: "tool-git-commit-lore-config-inline-enabled",
|
|
3705
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix: conventional"' },
|
|
3706
|
+
}, { cwd });
|
|
3707
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3708
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
3709
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3710
|
+
});
|
|
3711
|
+
});
|
|
3712
|
+
it("restores default-off Lore guard when env -u removes a disabled CODEX_HOME config source", async () => {
|
|
3713
|
+
await withLoreGuardConfig("0", "config-codex-home-unset", async (cwd) => {
|
|
3714
|
+
const result = await dispatchCodexNativeHook({
|
|
3715
|
+
hook_event_name: "PreToolUse",
|
|
3716
|
+
cwd,
|
|
3717
|
+
tool_name: "Bash",
|
|
3718
|
+
tool_use_id: "tool-git-commit-lore-config-codex-home-unset",
|
|
3719
|
+
tool_input: { command: 'env -u CODEX_HOME git commit -m "fix: conventional"' },
|
|
3720
|
+
}, { cwd });
|
|
3721
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3722
|
+
assert.equal(result.outputJson, null);
|
|
3723
|
+
});
|
|
3724
|
+
});
|
|
3080
3725
|
it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
|
|
3081
3726
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
|
|
3082
3727
|
try {
|
|
@@ -3094,7 +3739,30 @@ exit 0
|
|
|
3094
3739
|
await rm(cwd, { recursive: true, force: true });
|
|
3095
3740
|
}
|
|
3096
3741
|
});
|
|
3097
|
-
it("
|
|
3742
|
+
it("allows inline disabled guard to override an enabled process env", async () => {
|
|
3743
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-override-disabled-"));
|
|
3744
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3745
|
+
try {
|
|
3746
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
3747
|
+
const result = await dispatchCodexNativeHook({
|
|
3748
|
+
hook_event_name: "PreToolUse",
|
|
3749
|
+
cwd,
|
|
3750
|
+
tool_name: "Bash",
|
|
3751
|
+
tool_use_id: "tool-git-commit-lore-inline-override-disabled",
|
|
3752
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
|
|
3753
|
+
}, { cwd });
|
|
3754
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3755
|
+
assert.equal(result.outputJson, null);
|
|
3756
|
+
}
|
|
3757
|
+
finally {
|
|
3758
|
+
if (original === undefined)
|
|
3759
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3760
|
+
else
|
|
3761
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3762
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3763
|
+
}
|
|
3764
|
+
});
|
|
3765
|
+
it("does not treat newline-separated Lore guard assignment as inline git commit opt-in", async () => {
|
|
3098
3766
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
|
|
3099
3767
|
try {
|
|
3100
3768
|
const result = await dispatchCodexNativeHook({
|
|
@@ -3102,21 +3770,33 @@ exit 0
|
|
|
3102
3770
|
cwd,
|
|
3103
3771
|
tool_name: "Bash",
|
|
3104
3772
|
tool_use_id: "tool-git-commit-lore-newline-assignment",
|
|
3105
|
-
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=
|
|
3773
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1\ngit commit -m "fix: conventional"' },
|
|
3106
3774
|
}, { cwd });
|
|
3107
3775
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3108
|
-
assert.equal(result.outputJson
|
|
3109
|
-
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3776
|
+
assert.equal(result.outputJson, null);
|
|
3110
3777
|
}
|
|
3111
3778
|
finally {
|
|
3112
3779
|
await rm(cwd, { recursive: true, force: true });
|
|
3113
3780
|
}
|
|
3114
3781
|
});
|
|
3115
|
-
it("restores default-
|
|
3782
|
+
it("restores default-off Lore guard when env -u unsets a config.toml fallback", async () => {
|
|
3783
|
+
await withLoreGuardConfig("1", "config-env-unset", async (cwd) => {
|
|
3784
|
+
const result = await dispatchCodexNativeHook({
|
|
3785
|
+
hook_event_name: "PreToolUse",
|
|
3786
|
+
cwd,
|
|
3787
|
+
tool_name: "Bash",
|
|
3788
|
+
tool_use_id: "tool-git-commit-lore-config-env-unset",
|
|
3789
|
+
tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
|
|
3790
|
+
}, { cwd });
|
|
3791
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3792
|
+
assert.equal(result.outputJson, null);
|
|
3793
|
+
});
|
|
3794
|
+
});
|
|
3795
|
+
it("restores default-off Lore guard when env -u unsets an enabled process env", async () => {
|
|
3116
3796
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
|
|
3117
3797
|
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3118
3798
|
try {
|
|
3119
|
-
process.env.OMX_LORE_COMMIT_GUARD = "
|
|
3799
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
3120
3800
|
const result = await dispatchCodexNativeHook({
|
|
3121
3801
|
hook_event_name: "PreToolUse",
|
|
3122
3802
|
cwd,
|
|
@@ -3125,8 +3805,7 @@ exit 0
|
|
|
3125
3805
|
tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
|
|
3126
3806
|
}, { cwd });
|
|
3127
3807
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3128
|
-
assert.equal(result.outputJson
|
|
3129
|
-
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3808
|
+
assert.equal(result.outputJson, null);
|
|
3130
3809
|
}
|
|
3131
3810
|
finally {
|
|
3132
3811
|
if (original === undefined)
|
|
@@ -3136,11 +3815,11 @@ exit 0
|
|
|
3136
3815
|
await rm(cwd, { recursive: true, force: true });
|
|
3137
3816
|
}
|
|
3138
3817
|
});
|
|
3139
|
-
it("restores default-
|
|
3818
|
+
it("restores default-off Lore guard when env -i clears an enabled process env", async () => {
|
|
3140
3819
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
|
|
3141
3820
|
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3142
3821
|
try {
|
|
3143
|
-
process.env.OMX_LORE_COMMIT_GUARD = "
|
|
3822
|
+
process.env.OMX_LORE_COMMIT_GUARD = "1";
|
|
3144
3823
|
const result = await dispatchCodexNativeHook({
|
|
3145
3824
|
hook_event_name: "PreToolUse",
|
|
3146
3825
|
cwd,
|
|
@@ -3149,8 +3828,7 @@ exit 0
|
|
|
3149
3828
|
tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
|
|
3150
3829
|
}, { cwd });
|
|
3151
3830
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3152
|
-
assert.equal(result.outputJson
|
|
3153
|
-
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3831
|
+
assert.equal(result.outputJson, null);
|
|
3154
3832
|
}
|
|
3155
3833
|
finally {
|
|
3156
3834
|
if (original === undefined)
|
|
@@ -3160,7 +3838,7 @@ exit 0
|
|
|
3160
3838
|
await rm(cwd, { recursive: true, force: true });
|
|
3161
3839
|
}
|
|
3162
3840
|
});
|
|
3163
|
-
it("keeps Lore commit enforcement
|
|
3841
|
+
it("keeps Lore commit enforcement disabled for unknown inline guard values", async () => {
|
|
3164
3842
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
|
|
3165
3843
|
try {
|
|
3166
3844
|
const result = await dispatchCodexNativeHook({
|
|
@@ -3171,8 +3849,7 @@ exit 0
|
|
|
3171
3849
|
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
|
|
3172
3850
|
}, { cwd });
|
|
3173
3851
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3174
|
-
assert.equal(result.outputJson
|
|
3175
|
-
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3852
|
+
assert.equal(result.outputJson, null);
|
|
3176
3853
|
}
|
|
3177
3854
|
finally {
|
|
3178
3855
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -3201,7 +3878,7 @@ exit 0
|
|
|
3201
3878
|
await rm(cwd, { recursive: true, force: true });
|
|
3202
3879
|
}
|
|
3203
3880
|
});
|
|
3204
|
-
it("keeps Lore commit enforcement
|
|
3881
|
+
it("keeps Lore commit enforcement disabled for unknown guard values", async () => {
|
|
3205
3882
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
|
|
3206
3883
|
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3207
3884
|
try {
|
|
@@ -3214,8 +3891,7 @@ exit 0
|
|
|
3214
3891
|
tool_input: { command: 'git commit -m "fix tests"' },
|
|
3215
3892
|
}, { cwd });
|
|
3216
3893
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3217
|
-
assert.equal(result.outputJson
|
|
3218
|
-
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3894
|
+
assert.equal(result.outputJson, null);
|
|
3219
3895
|
}
|
|
3220
3896
|
finally {
|
|
3221
3897
|
if (original === undefined)
|
|
@@ -3308,7 +3984,7 @@ exit 0
|
|
|
3308
3984
|
cwd,
|
|
3309
3985
|
tool_name: "Bash",
|
|
3310
3986
|
tool_use_id: "tool-git-commit-env-invalid",
|
|
3311
|
-
tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
|
|
3987
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 HUSKY=0 git commit -m "fix tests"' },
|
|
3312
3988
|
}, { cwd });
|
|
3313
3989
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3314
3990
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3338,7 +4014,7 @@ exit 0
|
|
|
3338
4014
|
cwd,
|
|
3339
4015
|
tool_name: "Bash",
|
|
3340
4016
|
tool_use_id: "tool-git-commit-option-invalid",
|
|
3341
|
-
tool_input: { command: 'git -c core.editor=true commit -m "fix tests"' },
|
|
4017
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git -c core.editor=true commit -m "fix tests"' },
|
|
3342
4018
|
}, { cwd });
|
|
3343
4019
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3344
4020
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3368,7 +4044,7 @@ exit 0
|
|
|
3368
4044
|
cwd,
|
|
3369
4045
|
tool_name: "Bash",
|
|
3370
4046
|
tool_use_id: "tool-git-exe-commit-env-wrapper-invalid",
|
|
3371
|
-
tool_input: { command: 'env git.exe commit -m "fix tests"' },
|
|
4047
|
+
tool_input: { command: 'env OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
3372
4048
|
}, { cwd });
|
|
3373
4049
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3374
4050
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3398,7 +4074,7 @@ exit 0
|
|
|
3398
4074
|
cwd,
|
|
3399
4075
|
tool_name: "Bash",
|
|
3400
4076
|
tool_use_id: "tool-git-exe-commit-invalid",
|
|
3401
|
-
tool_input: { command: 'git.exe commit -m "fix tests"' },
|
|
4077
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
3402
4078
|
}, { cwd });
|
|
3403
4079
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3404
4080
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3428,7 +4104,7 @@ exit 0
|
|
|
3428
4104
|
cwd,
|
|
3429
4105
|
tool_name: "Bash",
|
|
3430
4106
|
tool_use_id: "tool-git-exe-commit-env-flag-wrapper-invalid",
|
|
3431
|
-
tool_input: { command: 'env -i PATH=/usr/bin git.exe commit -m "fix tests"' },
|
|
4107
|
+
tool_input: { command: 'env -i PATH=/usr/bin OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
3432
4108
|
}, { cwd });
|
|
3433
4109
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3434
4110
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3458,7 +4134,7 @@ exit 0
|
|
|
3458
4134
|
cwd,
|
|
3459
4135
|
tool_name: "Bash",
|
|
3460
4136
|
tool_use_id: "tool-git-exe-commit-env-value-wrapper-invalid",
|
|
3461
|
-
tool_input: { command: 'env -u FOO git.exe commit -m "fix tests"' },
|
|
4137
|
+
tool_input: { command: 'env -u FOO OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
|
|
3462
4138
|
}, { cwd });
|
|
3463
4139
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3464
4140
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3488,7 +4164,7 @@ exit 0
|
|
|
3488
4164
|
cwd,
|
|
3489
4165
|
tool_name: "Bash",
|
|
3490
4166
|
tool_use_id: "tool-git-exe-commit-windows-path-invalid",
|
|
3491
|
-
tool_input: { command: '"C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
|
|
4167
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
|
|
3492
4168
|
}, { cwd });
|
|
3493
4169
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3494
4170
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3518,7 +4194,7 @@ exit 0
|
|
|
3518
4194
|
cwd,
|
|
3519
4195
|
tool_name: "Bash",
|
|
3520
4196
|
tool_use_id: "tool-git-exe-commit-windows-backslash-path-invalid",
|
|
3521
|
-
tool_input: { command: '"C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
|
|
4197
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
|
|
3522
4198
|
}, { cwd });
|
|
3523
4199
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3524
4200
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3548,7 +4224,7 @@ exit 0
|
|
|
3548
4224
|
cwd,
|
|
3549
4225
|
tool_name: "Bash",
|
|
3550
4226
|
tool_use_id: "tool-git-commit-path-invalid",
|
|
3551
|
-
tool_input: { command: '/usr/bin/git commit -m "fix tests"' },
|
|
4227
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 /usr/bin/git commit -m "fix tests"' },
|
|
3552
4228
|
}, { cwd });
|
|
3553
4229
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3554
4230
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3578,7 +4254,7 @@ exit 0
|
|
|
3578
4254
|
cwd,
|
|
3579
4255
|
tool_name: "Bash",
|
|
3580
4256
|
tool_use_id: "tool-git-commit-file",
|
|
3581
|
-
tool_input: { command: "git commit -F .git/COMMIT_EDITMSG" },
|
|
4257
|
+
tool_input: { command: "OMX_LORE_COMMIT_GUARD=1 git commit -F .git/COMMIT_EDITMSG" },
|
|
3582
4258
|
}, { cwd });
|
|
3583
4259
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3584
4260
|
assert.deepEqual(result.outputJson, {
|
|
@@ -3607,7 +4283,7 @@ exit 0
|
|
|
3607
4283
|
tool_use_id: "tool-git-commit-missing-omx-coauthor",
|
|
3608
4284
|
tool_input: {
|
|
3609
4285
|
command: [
|
|
3610
|
-
'git commit',
|
|
4286
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
3611
4287
|
'-m "Prevent invalid history from bypassing Lore enforcement"',
|
|
3612
4288
|
'-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
|
|
3613
4289
|
'-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
|
|
@@ -3642,7 +4318,7 @@ exit 0
|
|
|
3642
4318
|
tool_use_id: "tool-git-commit-valid",
|
|
3643
4319
|
tool_input: {
|
|
3644
4320
|
command: [
|
|
3645
|
-
'git commit',
|
|
4321
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
3646
4322
|
'-m "Prevent invalid history from bypassing Lore enforcement"',
|
|
3647
4323
|
'-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
|
|
3648
4324
|
'-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
|
|
@@ -3668,7 +4344,7 @@ exit 0
|
|
|
3668
4344
|
tool_use_id: "tool-git-commit-compact-coauthor",
|
|
3669
4345
|
tool_input: {
|
|
3670
4346
|
command: [
|
|
3671
|
-
'git commit',
|
|
4347
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
3672
4348
|
'-m "Launch lvisai.xyz intro site"',
|
|
3673
4349
|
'-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
3674
4350
|
].join(" "),
|
|
@@ -3691,7 +4367,7 @@ exit 0
|
|
|
3691
4367
|
tool_use_id: "tool-git-commit-compact-trailers",
|
|
3692
4368
|
tool_input: {
|
|
3693
4369
|
command: [
|
|
3694
|
-
'git commit',
|
|
4370
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
3695
4371
|
'-m "Launch lvisai.xyz intro site"',
|
|
3696
4372
|
'-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>"',
|
|
3697
4373
|
].join(" "),
|
|
@@ -3714,7 +4390,7 @@ exit 0
|
|
|
3714
4390
|
tool_use_id: "tool-git-commit-compact-no-separator",
|
|
3715
4391
|
tool_input: {
|
|
3716
4392
|
command: [
|
|
3717
|
-
'git commit',
|
|
4393
|
+
'OMX_LORE_COMMIT_GUARD=1 git commit',
|
|
3718
4394
|
'--message="Launch lvisai.xyz intro site\nCo-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
3719
4395
|
].join(" "),
|
|
3720
4396
|
},
|
|
@@ -4052,41 +4728,36 @@ exit 0
|
|
|
4052
4728
|
await rm(cwd, { recursive: true, force: true });
|
|
4053
4729
|
}
|
|
4054
4730
|
});
|
|
4055
|
-
it("
|
|
4056
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-
|
|
4731
|
+
it("does not treat non-MCP source output containing detector constants as MCP transport death", async () => {
|
|
4732
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-read-mcp-source-"));
|
|
4057
4733
|
try {
|
|
4058
4734
|
const result = await dispatchCodexNativeHook({
|
|
4059
4735
|
hook_event_name: "PostToolUse",
|
|
4060
4736
|
cwd,
|
|
4061
|
-
tool_name: "
|
|
4062
|
-
tool_use_id: "tool-mcp-
|
|
4063
|
-
tool_input: {
|
|
4064
|
-
tool_response: "
|
|
4737
|
+
tool_name: "Read",
|
|
4738
|
+
tool_use_id: "tool-read-mcp-source",
|
|
4739
|
+
tool_input: { file_path: "src/scripts/codex-native-pre-post.ts" },
|
|
4740
|
+
tool_response: "const MCP_TRANSPORT_FAILURE_PATTERNS = [/transport closed/i, /server disconnected/i];\nconst context = /\\bmcp\\b/i;",
|
|
4065
4741
|
}, { cwd });
|
|
4066
4742
|
assert.equal(result.omxEventName, "post-tool-use");
|
|
4067
|
-
|
|
4068
|
-
assert.equal(output?.decision, "block");
|
|
4069
|
-
assert.equal(output?.reason, "The MCP tool appears to have lost its transport/server connection. Preserve state, debug the transport failure, and use OMX CLI/file-backed fallbacks instead of retrying blindly.");
|
|
4070
|
-
const additionalContext = String(output?.hookSpecificOutput?.additionalContext ?? "");
|
|
4071
|
-
assert.match(additionalContext, /omx state write --input/);
|
|
4072
|
-
assert.match(additionalContext, /plain Node stdio processes/i);
|
|
4073
|
-
assert.match(additionalContext, /read-stall-state/);
|
|
4074
|
-
assert.match(additionalContext, /OMX_MCP_TRANSPORT_DEBUG=1/);
|
|
4743
|
+
assert.equal(result.outputJson, null);
|
|
4075
4744
|
}
|
|
4076
4745
|
finally {
|
|
4077
4746
|
await rm(cwd, { recursive: true, force: true });
|
|
4078
4747
|
}
|
|
4079
4748
|
});
|
|
4080
|
-
it("does not
|
|
4081
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-
|
|
4749
|
+
it("does not treat non-MCP docs stdout mentioning closed MCP transport as transport death", async () => {
|
|
4750
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-docs-mcp-log-"));
|
|
4082
4751
|
try {
|
|
4083
4752
|
const result = await dispatchCodexNativeHook({
|
|
4084
4753
|
hook_event_name: "PostToolUse",
|
|
4085
4754
|
cwd,
|
|
4086
|
-
tool_name: "
|
|
4087
|
-
tool_use_id: "tool-mcp-
|
|
4088
|
-
|
|
4089
|
-
|
|
4755
|
+
tool_name: "ShellOutput",
|
|
4756
|
+
tool_use_id: "tool-docs-mcp-log",
|
|
4757
|
+
tool_response: JSON.stringify({
|
|
4758
|
+
stdout: "Troubleshooting note: MCP transport closed after the server disconnected in an old log.",
|
|
4759
|
+
stderr: "",
|
|
4760
|
+
}),
|
|
4090
4761
|
}, { cwd });
|
|
4091
4762
|
assert.equal(result.omxEventName, "post-tool-use");
|
|
4092
4763
|
assert.equal(result.outputJson, null);
|
|
@@ -4095,9 +4766,90 @@ exit 0
|
|
|
4095
4766
|
await rm(cwd, { recursive: true, force: true });
|
|
4096
4767
|
}
|
|
4097
4768
|
});
|
|
4098
|
-
it("
|
|
4099
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
4100
|
-
|
|
4769
|
+
it("does not MCP-block non-MCP command output with unrelated stderr and MCP transport stdout", async () => {
|
|
4770
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-nonmcp-mixed-output-"));
|
|
4771
|
+
try {
|
|
4772
|
+
const result = await dispatchCodexNativeHook({
|
|
4773
|
+
hook_event_name: "PostToolUse",
|
|
4774
|
+
cwd,
|
|
4775
|
+
tool_name: "ShellOutput",
|
|
4776
|
+
tool_use_id: "tool-nonmcp-mixed-output",
|
|
4777
|
+
tool_response: JSON.stringify({
|
|
4778
|
+
stdout: "captured log line: MCP transport closed before response",
|
|
4779
|
+
stderr: "grep: fixture.txt: No such file or directory",
|
|
4780
|
+
}),
|
|
4781
|
+
}, { cwd });
|
|
4782
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4783
|
+
assert.equal(result.outputJson, null);
|
|
4784
|
+
}
|
|
4785
|
+
finally {
|
|
4786
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4787
|
+
}
|
|
4788
|
+
});
|
|
4789
|
+
it("still blocks MCP-like raw transport failures", async () => {
|
|
4790
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-raw-transport-"));
|
|
4791
|
+
try {
|
|
4792
|
+
const result = await dispatchCodexNativeHook({
|
|
4793
|
+
hook_event_name: "PostToolUse",
|
|
4794
|
+
cwd,
|
|
4795
|
+
tool_name: "mcp__omx_state__state_write",
|
|
4796
|
+
tool_use_id: "tool-mcp-raw-transport",
|
|
4797
|
+
tool_response: "transport closed after server disconnected",
|
|
4798
|
+
}, { cwd });
|
|
4799
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4800
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
4801
|
+
assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
|
|
4802
|
+
}
|
|
4803
|
+
finally {
|
|
4804
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4805
|
+
}
|
|
4806
|
+
});
|
|
4807
|
+
it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
|
|
4808
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
|
|
4809
|
+
try {
|
|
4810
|
+
const result = await dispatchCodexNativeHook({
|
|
4811
|
+
hook_event_name: "PostToolUse",
|
|
4812
|
+
cwd,
|
|
4813
|
+
tool_name: "mcp__omx_state__state_write",
|
|
4814
|
+
tool_use_id: "tool-mcp-transport",
|
|
4815
|
+
tool_input: { mode: "team", active: true },
|
|
4816
|
+
tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
|
|
4817
|
+
}, { cwd });
|
|
4818
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4819
|
+
const output = result.outputJson;
|
|
4820
|
+
assert.equal(output?.decision, "block");
|
|
4821
|
+
assert.equal(output?.reason, "The MCP tool appears to have lost its transport/server connection. Preserve state, debug the transport failure, and use OMX CLI/file-backed fallbacks instead of retrying blindly.");
|
|
4822
|
+
const additionalContext = String(output?.hookSpecificOutput?.additionalContext ?? "");
|
|
4823
|
+
assert.match(additionalContext, /omx state write --input/);
|
|
4824
|
+
assert.match(additionalContext, /plain Node stdio processes/i);
|
|
4825
|
+
assert.match(additionalContext, /read-stall-state/);
|
|
4826
|
+
assert.match(additionalContext, /OMX_MCP_TRANSPORT_DEBUG=1/);
|
|
4827
|
+
}
|
|
4828
|
+
finally {
|
|
4829
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4830
|
+
}
|
|
4831
|
+
});
|
|
4832
|
+
it("does not classify non-transport MCP failures as transport death", async () => {
|
|
4833
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-nontransport-"));
|
|
4834
|
+
try {
|
|
4835
|
+
const result = await dispatchCodexNativeHook({
|
|
4836
|
+
hook_event_name: "PostToolUse",
|
|
4837
|
+
cwd,
|
|
4838
|
+
tool_name: "mcp__omx_state__state_write",
|
|
4839
|
+
tool_use_id: "tool-mcp-nontransport",
|
|
4840
|
+
tool_input: { active: true },
|
|
4841
|
+
tool_response: "{\"error\":\"validation failed\",\"details\":\"mode is required\"}",
|
|
4842
|
+
}, { cwd });
|
|
4843
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4844
|
+
assert.equal(result.outputJson, null);
|
|
4845
|
+
}
|
|
4846
|
+
finally {
|
|
4847
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4848
|
+
}
|
|
4849
|
+
});
|
|
4850
|
+
it("marks active team state failed on MCP transport death without deleting team state", async () => {
|
|
4851
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-mcp-transport-"));
|
|
4852
|
+
const previousCwd = process.cwd();
|
|
4101
4853
|
try {
|
|
4102
4854
|
process.chdir(cwd);
|
|
4103
4855
|
await initTeamState("transport-team", "task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-transport" });
|
|
@@ -4366,6 +5118,51 @@ exit 0
|
|
|
4366
5118
|
await rm(cwd, { recursive: true, force: true });
|
|
4367
5119
|
}
|
|
4368
5120
|
});
|
|
5121
|
+
it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
|
|
5122
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
|
|
5123
|
+
try {
|
|
5124
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5125
|
+
const sessionId = "sess-stop-autopilot-review-compact";
|
|
5126
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5127
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
5128
|
+
active: true,
|
|
5129
|
+
mode: "autopilot",
|
|
5130
|
+
current_phase: "code-review",
|
|
5131
|
+
state: {
|
|
5132
|
+
phase_cycle: ["ralplan", "ralph", "code-review"],
|
|
5133
|
+
handoff_artifacts: {
|
|
5134
|
+
ralplan: ".omx/plans/prd-issue-2366.md",
|
|
5135
|
+
ralph: { verification: ["npm test"] },
|
|
5136
|
+
code_review: null,
|
|
5137
|
+
},
|
|
5138
|
+
review_verdict: null,
|
|
5139
|
+
},
|
|
5140
|
+
});
|
|
5141
|
+
const compactBoundary = await dispatchCodexNativeHook({
|
|
5142
|
+
hook_event_name: "Stop",
|
|
5143
|
+
cwd,
|
|
5144
|
+
session_id: sessionId,
|
|
5145
|
+
stop_reason: "context compact",
|
|
5146
|
+
}, { cwd });
|
|
5147
|
+
const resumedStop = await dispatchCodexNativeHook({
|
|
5148
|
+
hook_event_name: "Stop",
|
|
5149
|
+
cwd,
|
|
5150
|
+
session_id: sessionId,
|
|
5151
|
+
}, { cwd });
|
|
5152
|
+
assert.equal(compactBoundary.omxEventName, "stop");
|
|
5153
|
+
assert.equal(compactBoundary.outputJson, null);
|
|
5154
|
+
assert.equal(resumedStop.omxEventName, "stop");
|
|
5155
|
+
assert.deepEqual(resumedStop.outputJson, {
|
|
5156
|
+
decision: "block",
|
|
5157
|
+
reason: "OMX autopilot is still active (phase: code-review); continue the task and gather fresh verification evidence before stopping.",
|
|
5158
|
+
stopReason: "autopilot_code-review",
|
|
5159
|
+
systemMessage: "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state.",
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
finally {
|
|
5163
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5164
|
+
}
|
|
5165
|
+
});
|
|
4369
5166
|
it("suppresses duplicate Autopilot planning Stop replays so stale planning state cannot loop indefinitely", async () => {
|
|
4370
5167
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
|
|
4371
5168
|
try {
|
|
@@ -4402,6 +5199,80 @@ exit 0
|
|
|
4402
5199
|
await rm(cwd, { recursive: true, force: true });
|
|
4403
5200
|
}
|
|
4404
5201
|
});
|
|
5202
|
+
it("allows Stop when terminal Autopilot run-state shadows stale session ralplan state", async () => {
|
|
5203
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-terminal-run-state-"));
|
|
5204
|
+
try {
|
|
5205
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5206
|
+
const sessionId = "sess-stop-autopilot-terminal-run-state";
|
|
5207
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5208
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
5209
|
+
active: true,
|
|
5210
|
+
mode: "autopilot",
|
|
5211
|
+
current_phase: "ralplan",
|
|
5212
|
+
});
|
|
5213
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
5214
|
+
version: 1,
|
|
5215
|
+
active: false,
|
|
5216
|
+
mode: "autopilot",
|
|
5217
|
+
outcome: "finish",
|
|
5218
|
+
lifecycle_outcome: "finished",
|
|
5219
|
+
current_phase: "complete",
|
|
5220
|
+
completed_at: "2026-05-20T11:00:00.000Z",
|
|
5221
|
+
updated_at: "2026-05-20T11:00:00.000Z",
|
|
5222
|
+
});
|
|
5223
|
+
const result = await dispatchCodexNativeHook({
|
|
5224
|
+
hook_event_name: "Stop",
|
|
5225
|
+
cwd,
|
|
5226
|
+
session_id: sessionId,
|
|
5227
|
+
thread_id: "thread-stop-autopilot-terminal-run-state",
|
|
5228
|
+
turn_id: "turn-stop-autopilot-terminal-run-state-1",
|
|
5229
|
+
last_assistant_message: "Done. Verification passed.",
|
|
5230
|
+
}, { cwd });
|
|
5231
|
+
assert.equal(result.omxEventName, "stop");
|
|
5232
|
+
assert.equal(result.outputJson, null);
|
|
5233
|
+
}
|
|
5234
|
+
finally {
|
|
5235
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5236
|
+
}
|
|
5237
|
+
});
|
|
5238
|
+
it("still blocks Stop while Autopilot ralplan state is genuinely non-terminal", async () => {
|
|
5239
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-active-ralplan-"));
|
|
5240
|
+
try {
|
|
5241
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5242
|
+
const sessionId = "sess-stop-autopilot-active-ralplan";
|
|
5243
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5244
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
5245
|
+
active: true,
|
|
5246
|
+
mode: "autopilot",
|
|
5247
|
+
current_phase: "ralplan",
|
|
5248
|
+
});
|
|
5249
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
5250
|
+
version: 1,
|
|
5251
|
+
active: true,
|
|
5252
|
+
mode: "autopilot",
|
|
5253
|
+
outcome: "continue",
|
|
5254
|
+
current_phase: "ralplan",
|
|
5255
|
+
updated_at: "2026-05-20T11:00:00.000Z",
|
|
5256
|
+
});
|
|
5257
|
+
const result = await dispatchCodexNativeHook({
|
|
5258
|
+
hook_event_name: "Stop",
|
|
5259
|
+
cwd,
|
|
5260
|
+
session_id: sessionId,
|
|
5261
|
+
thread_id: "thread-stop-autopilot-active-ralplan",
|
|
5262
|
+
turn_id: "turn-stop-autopilot-active-ralplan-1",
|
|
5263
|
+
}, { cwd });
|
|
5264
|
+
assert.equal(result.omxEventName, "stop");
|
|
5265
|
+
assert.deepEqual(result.outputJson, {
|
|
5266
|
+
decision: "block",
|
|
5267
|
+
reason: "OMX autopilot is still active (phase: ralplan); continue the task and gather fresh verification evidence before stopping.",
|
|
5268
|
+
stopReason: "autopilot_ralplan",
|
|
5269
|
+
systemMessage: "OMX autopilot is still active (phase: ralplan).",
|
|
5270
|
+
});
|
|
5271
|
+
}
|
|
5272
|
+
finally {
|
|
5273
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5274
|
+
}
|
|
5275
|
+
});
|
|
4405
5276
|
it("does not block Stop from stale root Autopilot planning state when the explicit session has no scoped state", async () => {
|
|
4406
5277
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autopilot-planning-"));
|
|
4407
5278
|
try {
|
|
@@ -4858,6 +5729,87 @@ exit 0
|
|
|
4858
5729
|
await rm(cwd, { recursive: true, force: true });
|
|
4859
5730
|
}
|
|
4860
5731
|
});
|
|
5732
|
+
it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
|
|
5733
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
|
|
5734
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5735
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5736
|
+
const prevPath = process.env.PATH;
|
|
5737
|
+
try {
|
|
5738
|
+
await initTeamState("worker-stop-team-busy-leader", "worker stop busy leader", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-busy-leader" });
|
|
5739
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5740
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
5741
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5742
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { busyLeader: true }));
|
|
5743
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5744
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5745
|
+
const teamDir = join(stateDir, "team", "worker-stop-team-busy-leader");
|
|
5746
|
+
const workerDir = join(teamDir, "workers", "worker-1");
|
|
5747
|
+
await writeJson(join(teamDir, "config.json"), {
|
|
5748
|
+
name: "worker-stop-team-busy-leader",
|
|
5749
|
+
tmux_session: "omx-team-worker-stop",
|
|
5750
|
+
leader_pane_id: "%42",
|
|
5751
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5752
|
+
});
|
|
5753
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
5754
|
+
name: "worker-stop-team-busy-leader",
|
|
5755
|
+
tmux_session: "omx-team-worker-stop",
|
|
5756
|
+
leader_pane_id: "%42",
|
|
5757
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5758
|
+
});
|
|
5759
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5760
|
+
name: "worker-1",
|
|
5761
|
+
index: 1,
|
|
5762
|
+
role: "executor",
|
|
5763
|
+
assigned_tasks: ["1"],
|
|
5764
|
+
worktree_path: cwd,
|
|
5765
|
+
team_state_root: stateDir,
|
|
5766
|
+
});
|
|
5767
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
5768
|
+
state: "done",
|
|
5769
|
+
current_task_id: "1",
|
|
5770
|
+
updated_at: new Date().toISOString(),
|
|
5771
|
+
});
|
|
5772
|
+
await writeJson(join(teamDir, "tasks", "task-1.json"), {
|
|
5773
|
+
id: "1",
|
|
5774
|
+
subject: "hook task",
|
|
5775
|
+
description: "finish hook task",
|
|
5776
|
+
status: "completed",
|
|
5777
|
+
owner: "worker-1",
|
|
5778
|
+
created_at: new Date().toISOString(),
|
|
5779
|
+
});
|
|
5780
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-team-busy-leader/worker-1";
|
|
5781
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5782
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
5783
|
+
const result = await dispatchCodexNativeHook({
|
|
5784
|
+
hook_event_name: "Stop",
|
|
5785
|
+
cwd,
|
|
5786
|
+
session_id: "sess-stop-team-worker-busy-leader",
|
|
5787
|
+
}, { cwd });
|
|
5788
|
+
assert.equal(result.outputJson, null);
|
|
5789
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
5790
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
5791
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
5792
|
+
assert.match(tmuxLog, /send-keys -t %42 C-m/);
|
|
5793
|
+
assert.ok(tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"), "busy worker-stop nudge should press Tab before C-m");
|
|
5794
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
5795
|
+
assert.equal(nudgeState.delivery, "queued");
|
|
5796
|
+
}
|
|
5797
|
+
finally {
|
|
5798
|
+
if (typeof prevTeamWorker === "string")
|
|
5799
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5800
|
+
else
|
|
5801
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
5802
|
+
if (typeof prevTeamStateRoot === "string")
|
|
5803
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5804
|
+
else
|
|
5805
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5806
|
+
if (typeof prevPath === "string")
|
|
5807
|
+
process.env.PATH = prevPath;
|
|
5808
|
+
else
|
|
5809
|
+
delete process.env.PATH;
|
|
5810
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5811
|
+
}
|
|
5812
|
+
});
|
|
4861
5813
|
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
4862
5814
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
4863
5815
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
@@ -5622,13 +6574,25 @@ exit 0
|
|
|
5622
6574
|
await rm(cwd, { recursive: true, force: true });
|
|
5623
6575
|
}
|
|
5624
6576
|
});
|
|
5625
|
-
it("does not block
|
|
5626
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
6577
|
+
it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
|
|
6578
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
|
|
5627
6579
|
try {
|
|
5628
6580
|
const stateDir = join(cwd, ".omx", "state");
|
|
5629
|
-
const sessionId = "sess-stop-
|
|
6581
|
+
const sessionId = "sess-stop-stale-session-ralplan";
|
|
5630
6582
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5631
6583
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6584
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6585
|
+
active: false,
|
|
6586
|
+
skill: "ralplan",
|
|
6587
|
+
phase: "reviewing",
|
|
6588
|
+
active_skills: [],
|
|
6589
|
+
});
|
|
6590
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
6591
|
+
active: false,
|
|
6592
|
+
mode: "ralplan",
|
|
6593
|
+
current_phase: "complete",
|
|
6594
|
+
session_id: sessionId,
|
|
6595
|
+
});
|
|
5632
6596
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
5633
6597
|
active: true,
|
|
5634
6598
|
skill: "ralplan",
|
|
@@ -5647,16 +6611,6 @@ exit 0
|
|
|
5647
6611
|
current_phase: "planning",
|
|
5648
6612
|
session_id: sessionId,
|
|
5649
6613
|
});
|
|
5650
|
-
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
5651
|
-
version: 1,
|
|
5652
|
-
mode: "ralplan",
|
|
5653
|
-
active: false,
|
|
5654
|
-
outcome: "finish",
|
|
5655
|
-
lifecycle_outcome: "finished",
|
|
5656
|
-
current_phase: "complete",
|
|
5657
|
-
completed_at: "2026-05-01T00:00:00.000Z",
|
|
5658
|
-
updated_at: "2026-05-01T00:00:00.000Z",
|
|
5659
|
-
});
|
|
5660
6614
|
const result = await dispatchCodexNativeHook({
|
|
5661
6615
|
hook_event_name: "Stop",
|
|
5662
6616
|
cwd,
|
|
@@ -5669,13 +6623,26 @@ exit 0
|
|
|
5669
6623
|
await rm(cwd, { recursive: true, force: true });
|
|
5670
6624
|
}
|
|
5671
6625
|
});
|
|
5672
|
-
it("
|
|
5673
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
6626
|
+
it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
|
|
6627
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
|
|
5674
6628
|
try {
|
|
5675
6629
|
const stateDir = join(cwd, ".omx", "state");
|
|
5676
|
-
const sessionId = "sess-stop-current-ralplan";
|
|
6630
|
+
const sessionId = "sess-stop-current-active-ralplan";
|
|
5677
6631
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5678
6632
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6633
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6634
|
+
active: false,
|
|
6635
|
+
skill: "ralplan",
|
|
6636
|
+
phase: "complete",
|
|
6637
|
+
session_id: "sess-stop-old-ralplan",
|
|
6638
|
+
active_skills: [],
|
|
6639
|
+
});
|
|
6640
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
6641
|
+
active: false,
|
|
6642
|
+
mode: "ralplan",
|
|
6643
|
+
current_phase: "complete",
|
|
6644
|
+
session_id: "sess-stop-old-ralplan",
|
|
6645
|
+
});
|
|
5679
6646
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
5680
6647
|
active: true,
|
|
5681
6648
|
skill: "ralplan",
|
|
@@ -5692,7 +6659,7 @@ exit 0
|
|
|
5692
6659
|
active: true,
|
|
5693
6660
|
mode: "ralplan",
|
|
5694
6661
|
current_phase: "planning",
|
|
5695
|
-
session_id:
|
|
6662
|
+
session_id: sessionId,
|
|
5696
6663
|
});
|
|
5697
6664
|
const result = await dispatchCodexNativeHook({
|
|
5698
6665
|
hook_event_name: "Stop",
|
|
@@ -5700,18 +6667,257 @@ exit 0
|
|
|
5700
6667
|
session_id: sessionId,
|
|
5701
6668
|
}, { cwd });
|
|
5702
6669
|
assert.equal(result.omxEventName, "stop");
|
|
5703
|
-
assert.equal(result.outputJson,
|
|
6670
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6671
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
5704
6672
|
}
|
|
5705
6673
|
finally {
|
|
5706
6674
|
await rm(cwd, { recursive: true, force: true });
|
|
5707
6675
|
}
|
|
5708
6676
|
});
|
|
5709
|
-
it("
|
|
5710
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
6677
|
+
it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
|
|
6678
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
|
|
5711
6679
|
try {
|
|
5712
6680
|
const stateDir = join(cwd, ".omx", "state");
|
|
5713
|
-
|
|
5714
|
-
await
|
|
6681
|
+
const sessionId = "sess-stop-unscoped-root-current-active";
|
|
6682
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6683
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6684
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6685
|
+
active: false,
|
|
6686
|
+
skill: "ralplan",
|
|
6687
|
+
phase: "complete",
|
|
6688
|
+
active_skills: [],
|
|
6689
|
+
});
|
|
6690
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
6691
|
+
active: false,
|
|
6692
|
+
mode: "ralplan",
|
|
6693
|
+
current_phase: "complete",
|
|
6694
|
+
});
|
|
6695
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6696
|
+
active: true,
|
|
6697
|
+
skill: "ralplan",
|
|
6698
|
+
phase: "planning",
|
|
6699
|
+
session_id: sessionId,
|
|
6700
|
+
active_skills: [{
|
|
6701
|
+
skill: "ralplan",
|
|
6702
|
+
phase: "planning",
|
|
6703
|
+
active: true,
|
|
6704
|
+
session_id: sessionId,
|
|
6705
|
+
}],
|
|
6706
|
+
});
|
|
6707
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6708
|
+
active: true,
|
|
6709
|
+
mode: "ralplan",
|
|
6710
|
+
current_phase: "planning",
|
|
6711
|
+
session_id: sessionId,
|
|
6712
|
+
});
|
|
6713
|
+
const result = await dispatchCodexNativeHook({
|
|
6714
|
+
hook_event_name: "Stop",
|
|
6715
|
+
cwd,
|
|
6716
|
+
session_id: sessionId,
|
|
6717
|
+
}, { cwd });
|
|
6718
|
+
assert.equal(result.omxEventName, "stop");
|
|
6719
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6720
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
6721
|
+
}
|
|
6722
|
+
finally {
|
|
6723
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6724
|
+
}
|
|
6725
|
+
});
|
|
6726
|
+
it("does not block stale session ralplan when root ralplan is terminal and another root skill is active", async () => {
|
|
6727
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-ralplan-other-root-skill-"));
|
|
6728
|
+
try {
|
|
6729
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6730
|
+
const sessionId = "sess-stop-stale-ralplan-other-root-skill";
|
|
6731
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6732
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6733
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6734
|
+
active: true,
|
|
6735
|
+
skill: "deep-interview",
|
|
6736
|
+
phase: "intent-first",
|
|
6737
|
+
session_id: sessionId,
|
|
6738
|
+
active_skills: [{
|
|
6739
|
+
skill: "deep-interview",
|
|
6740
|
+
phase: "intent-first",
|
|
6741
|
+
active: true,
|
|
6742
|
+
session_id: sessionId,
|
|
6743
|
+
}],
|
|
6744
|
+
});
|
|
6745
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
6746
|
+
active: false,
|
|
6747
|
+
mode: "ralplan",
|
|
6748
|
+
current_phase: "complete",
|
|
6749
|
+
session_id: sessionId,
|
|
6750
|
+
});
|
|
6751
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6752
|
+
active: true,
|
|
6753
|
+
skill: "ralplan",
|
|
6754
|
+
phase: "planning",
|
|
6755
|
+
session_id: sessionId,
|
|
6756
|
+
active_skills: [{
|
|
6757
|
+
skill: "ralplan",
|
|
6758
|
+
phase: "planning",
|
|
6759
|
+
active: true,
|
|
6760
|
+
session_id: sessionId,
|
|
6761
|
+
}],
|
|
6762
|
+
});
|
|
6763
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6764
|
+
active: true,
|
|
6765
|
+
mode: "ralplan",
|
|
6766
|
+
current_phase: "planning",
|
|
6767
|
+
session_id: sessionId,
|
|
6768
|
+
});
|
|
6769
|
+
const result = await dispatchCodexNativeHook({
|
|
6770
|
+
hook_event_name: "Stop",
|
|
6771
|
+
cwd,
|
|
6772
|
+
session_id: sessionId,
|
|
6773
|
+
}, { cwd });
|
|
6774
|
+
assert.equal(result.omxEventName, "stop");
|
|
6775
|
+
assert.equal(result.outputJson, null);
|
|
6776
|
+
}
|
|
6777
|
+
finally {
|
|
6778
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6779
|
+
}
|
|
6780
|
+
});
|
|
6781
|
+
it("keeps blocking session ralplan when canonical root state is not inactive", async () => {
|
|
6782
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-active-"));
|
|
6783
|
+
try {
|
|
6784
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6785
|
+
const sessionId = "sess-stop-session-ralplan-root-active";
|
|
6786
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6787
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6788
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6789
|
+
active: true,
|
|
6790
|
+
skill: "ralplan",
|
|
6791
|
+
phase: "planning",
|
|
6792
|
+
session_id: sessionId,
|
|
6793
|
+
active_skills: [{
|
|
6794
|
+
skill: "ralplan",
|
|
6795
|
+
phase: "planning",
|
|
6796
|
+
active: true,
|
|
6797
|
+
session_id: sessionId,
|
|
6798
|
+
}],
|
|
6799
|
+
});
|
|
6800
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6801
|
+
active: true,
|
|
6802
|
+
skill: "ralplan",
|
|
6803
|
+
phase: "planning",
|
|
6804
|
+
session_id: sessionId,
|
|
6805
|
+
active_skills: [{
|
|
6806
|
+
skill: "ralplan",
|
|
6807
|
+
phase: "planning",
|
|
6808
|
+
active: true,
|
|
6809
|
+
session_id: sessionId,
|
|
6810
|
+
}],
|
|
6811
|
+
});
|
|
6812
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6813
|
+
active: true,
|
|
6814
|
+
mode: "ralplan",
|
|
6815
|
+
current_phase: "planning",
|
|
6816
|
+
session_id: sessionId,
|
|
6817
|
+
});
|
|
6818
|
+
const result = await dispatchCodexNativeHook({
|
|
6819
|
+
hook_event_name: "Stop",
|
|
6820
|
+
cwd,
|
|
6821
|
+
session_id: sessionId,
|
|
6822
|
+
}, { cwd });
|
|
6823
|
+
assert.equal(result.omxEventName, "stop");
|
|
6824
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6825
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
6826
|
+
}
|
|
6827
|
+
finally {
|
|
6828
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6829
|
+
}
|
|
6830
|
+
});
|
|
6831
|
+
it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
|
|
6832
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
|
|
6833
|
+
try {
|
|
6834
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6835
|
+
const sessionId = "sess-stop-terminal-ralplan";
|
|
6836
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6837
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6838
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6839
|
+
active: true,
|
|
6840
|
+
skill: "ralplan",
|
|
6841
|
+
phase: "planning",
|
|
6842
|
+
session_id: sessionId,
|
|
6843
|
+
active_skills: [{
|
|
6844
|
+
skill: "ralplan",
|
|
6845
|
+
phase: "planning",
|
|
6846
|
+
active: true,
|
|
6847
|
+
session_id: sessionId,
|
|
6848
|
+
}],
|
|
6849
|
+
});
|
|
6850
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6851
|
+
active: true,
|
|
6852
|
+
mode: "ralplan",
|
|
6853
|
+
current_phase: "planning",
|
|
6854
|
+
session_id: sessionId,
|
|
6855
|
+
});
|
|
6856
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
6857
|
+
version: 1,
|
|
6858
|
+
mode: "ralplan",
|
|
6859
|
+
active: false,
|
|
6860
|
+
outcome: "finish",
|
|
6861
|
+
lifecycle_outcome: "finished",
|
|
6862
|
+
current_phase: "complete",
|
|
6863
|
+
completed_at: "2026-05-01T00:00:00.000Z",
|
|
6864
|
+
updated_at: "2026-05-01T00:00:00.000Z",
|
|
6865
|
+
});
|
|
6866
|
+
const result = await dispatchCodexNativeHook({
|
|
6867
|
+
hook_event_name: "Stop",
|
|
6868
|
+
cwd,
|
|
6869
|
+
session_id: sessionId,
|
|
6870
|
+
}, { cwd });
|
|
6871
|
+
assert.equal(result.omxEventName, "stop");
|
|
6872
|
+
assert.equal(result.outputJson, null);
|
|
6873
|
+
}
|
|
6874
|
+
finally {
|
|
6875
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6876
|
+
}
|
|
6877
|
+
});
|
|
6878
|
+
it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
|
|
6879
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
|
|
6880
|
+
try {
|
|
6881
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6882
|
+
const sessionId = "sess-stop-current-ralplan";
|
|
6883
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6884
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6885
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6886
|
+
active: true,
|
|
6887
|
+
skill: "ralplan",
|
|
6888
|
+
phase: "planning",
|
|
6889
|
+
session_id: sessionId,
|
|
6890
|
+
active_skills: [{
|
|
6891
|
+
skill: "ralplan",
|
|
6892
|
+
phase: "planning",
|
|
6893
|
+
active: true,
|
|
6894
|
+
session_id: sessionId,
|
|
6895
|
+
}],
|
|
6896
|
+
});
|
|
6897
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6898
|
+
active: true,
|
|
6899
|
+
mode: "ralplan",
|
|
6900
|
+
current_phase: "planning",
|
|
6901
|
+
session_id: "sess-other-ralplan",
|
|
6902
|
+
});
|
|
6903
|
+
const result = await dispatchCodexNativeHook({
|
|
6904
|
+
hook_event_name: "Stop",
|
|
6905
|
+
cwd,
|
|
6906
|
+
session_id: sessionId,
|
|
6907
|
+
}, { cwd });
|
|
6908
|
+
assert.equal(result.omxEventName, "stop");
|
|
6909
|
+
assert.equal(result.outputJson, null);
|
|
6910
|
+
}
|
|
6911
|
+
finally {
|
|
6912
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6913
|
+
}
|
|
6914
|
+
});
|
|
6915
|
+
it("returns an explicit ralplan waiting status while subagents are still active", async () => {
|
|
6916
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
|
|
6917
|
+
try {
|
|
6918
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6919
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent"), { recursive: true });
|
|
6920
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent" });
|
|
5715
6921
|
await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "skill-active-state.json"), {
|
|
5716
6922
|
active: true,
|
|
5717
6923
|
skill: "ralplan",
|
|
@@ -6406,16 +7612,25 @@ exit 0
|
|
|
6406
7612
|
assert.equal(result.omxEventName, "stop");
|
|
6407
7613
|
const reason = String(result.outputJson?.reason);
|
|
6408
7614
|
assert.match(reason, /Ralph completion audit is missing required evidence/);
|
|
6409
|
-
assert.match(reason, /
|
|
7615
|
+
assert.match(reason, /set "completion_audit" on the Ralph state object/);
|
|
7616
|
+
assert.doesNotMatch(reason, /state\.completion_audit/);
|
|
6410
7617
|
assert.match(reason, /repo-relative JSON file/);
|
|
6411
7618
|
assert.match(reason, /Markdown artifacts and flat top-level checklist\/evidence fields are not accepted/);
|
|
6412
7619
|
assert.equal(result.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
|
|
6413
7620
|
const reopened = JSON.parse(await readFile(statePath, "utf-8"));
|
|
6414
|
-
assert.equal(reopened.active,
|
|
6415
|
-
assert.equal(reopened.current_phase, "
|
|
7621
|
+
assert.equal(reopened.active, false);
|
|
7622
|
+
assert.equal(reopened.current_phase, "complete");
|
|
6416
7623
|
assert.equal(reopened.completion_audit_gate, "blocked");
|
|
6417
7624
|
assert.equal(reopened.completion_audit_missing_reason, "missing_completion_audit");
|
|
6418
|
-
assert.equal(
|
|
7625
|
+
assert.equal(reopened.completed_at, "2026-05-10T12:00:00.000Z");
|
|
7626
|
+
const repeat = await dispatchCodexNativeHook({
|
|
7627
|
+
hook_event_name: "Stop",
|
|
7628
|
+
cwd,
|
|
7629
|
+
session_id: sessionId,
|
|
7630
|
+
last_assistant_message: "Done. Ralph complete.",
|
|
7631
|
+
}, { cwd });
|
|
7632
|
+
assert.equal(repeat.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
|
|
7633
|
+
assert.doesNotMatch(String(repeat.outputJson?.reason), /Ralph is still active/);
|
|
6419
7634
|
}
|
|
6420
7635
|
finally {
|
|
6421
7636
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -6605,6 +7820,42 @@ exit 0
|
|
|
6605
7820
|
await rm(cwd, { recursive: true, force: true });
|
|
6606
7821
|
}
|
|
6607
7822
|
});
|
|
7823
|
+
it("allows Stop from stale orphaned session-scoped Ralph starting iteration zero state", async () => {
|
|
7824
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-orphan-starting-ralph-"));
|
|
7825
|
+
try {
|
|
7826
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7827
|
+
const sessionId = "sess-stale-orphan-ralph";
|
|
7828
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
7829
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
|
|
7830
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
7831
|
+
active: true,
|
|
7832
|
+
mode: "ralph",
|
|
7833
|
+
current_phase: "starting",
|
|
7834
|
+
iteration: 0,
|
|
7835
|
+
session_id: sessionId,
|
|
7836
|
+
updated_at: "2000-01-01T00:00:00.000Z",
|
|
7837
|
+
});
|
|
7838
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7839
|
+
active: true,
|
|
7840
|
+
skill: "ralph",
|
|
7841
|
+
phase: "starting",
|
|
7842
|
+
session_id: sessionId,
|
|
7843
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
|
|
7844
|
+
});
|
|
7845
|
+
const result = await dispatchCodexNativeHook({
|
|
7846
|
+
hook_event_name: "Stop",
|
|
7847
|
+
cwd,
|
|
7848
|
+
session_id: sessionId,
|
|
7849
|
+
thread_id: "thread-verifier-terminal",
|
|
7850
|
+
last_assistant_message: "APPROVE: read-only verifier evidence is fresh and sufficient.",
|
|
7851
|
+
}, { cwd });
|
|
7852
|
+
assert.equal(result.omxEventName, "stop");
|
|
7853
|
+
assert.equal(result.outputJson, null);
|
|
7854
|
+
}
|
|
7855
|
+
finally {
|
|
7856
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7857
|
+
}
|
|
7858
|
+
});
|
|
6608
7859
|
it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
|
|
6609
7860
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
|
|
6610
7861
|
try {
|
|
@@ -6640,6 +7891,126 @@ exit 0
|
|
|
6640
7891
|
await rm(cwd, { recursive: true, force: true });
|
|
6641
7892
|
}
|
|
6642
7893
|
});
|
|
7894
|
+
it("retires prompt-seeded Ralph starting state when canonical Ralph already completed with audit", async () => {
|
|
7895
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-starting-"));
|
|
7896
|
+
try {
|
|
7897
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7898
|
+
const nativeSessionId = "native-hook-seed";
|
|
7899
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
7900
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
7901
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
7902
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
7903
|
+
session_id: canonicalSessionId,
|
|
7904
|
+
cwd,
|
|
7905
|
+
});
|
|
7906
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
7907
|
+
active: true,
|
|
7908
|
+
mode: "ralph",
|
|
7909
|
+
current_phase: "starting",
|
|
7910
|
+
session_id: nativeSessionId,
|
|
7911
|
+
iteration: 0,
|
|
7912
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
7913
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
7914
|
+
});
|
|
7915
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
7916
|
+
active: true,
|
|
7917
|
+
skill: "ralph",
|
|
7918
|
+
phase: "starting",
|
|
7919
|
+
session_id: nativeSessionId,
|
|
7920
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
7921
|
+
});
|
|
7922
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
7923
|
+
active: false,
|
|
7924
|
+
mode: "ralph",
|
|
7925
|
+
current_phase: "complete",
|
|
7926
|
+
session_id: canonicalSessionId,
|
|
7927
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
7928
|
+
completion_audit: {
|
|
7929
|
+
passed: true,
|
|
7930
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
7931
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
7932
|
+
},
|
|
7933
|
+
});
|
|
7934
|
+
const result = await dispatchCodexNativeHook({
|
|
7935
|
+
hook_event_name: "Stop",
|
|
7936
|
+
cwd,
|
|
7937
|
+
session_id: nativeSessionId,
|
|
7938
|
+
}, { cwd });
|
|
7939
|
+
assert.equal(result.omxEventName, "stop");
|
|
7940
|
+
assert.equal(result.outputJson, null);
|
|
7941
|
+
const retiredState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
7942
|
+
assert.equal(retiredState.active, false);
|
|
7943
|
+
assert.equal(retiredState.current_phase, "complete");
|
|
7944
|
+
assert.equal(retiredState.stop_reason, "shadowed_by_completed_canonical_ralph");
|
|
7945
|
+
assert.equal(retiredState.shadowed_by_completed_canonical_ralph.session_id, canonicalSessionId);
|
|
7946
|
+
}
|
|
7947
|
+
finally {
|
|
7948
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7949
|
+
}
|
|
7950
|
+
});
|
|
7951
|
+
it("does not retire prompt-seeded Ralph starting state from a completed canonical Ralph owned by another thread", async () => {
|
|
7952
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-thread-mismatch-"));
|
|
7953
|
+
try {
|
|
7954
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7955
|
+
const nativeSessionId = "native-hook-seed";
|
|
7956
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
7957
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
7958
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
7959
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
7960
|
+
session_id: canonicalSessionId,
|
|
7961
|
+
cwd,
|
|
7962
|
+
});
|
|
7963
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
7964
|
+
active: true,
|
|
7965
|
+
mode: "ralph",
|
|
7966
|
+
current_phase: "starting",
|
|
7967
|
+
session_id: nativeSessionId,
|
|
7968
|
+
iteration: 0,
|
|
7969
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
7970
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
7971
|
+
});
|
|
7972
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
7973
|
+
active: true,
|
|
7974
|
+
skill: "ralph",
|
|
7975
|
+
phase: "starting",
|
|
7976
|
+
session_id: nativeSessionId,
|
|
7977
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
7978
|
+
});
|
|
7979
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
7980
|
+
active: false,
|
|
7981
|
+
mode: "ralph",
|
|
7982
|
+
current_phase: "complete",
|
|
7983
|
+
session_id: canonicalSessionId,
|
|
7984
|
+
owner_codex_thread_id: "thread-A",
|
|
7985
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
7986
|
+
completion_audit: {
|
|
7987
|
+
passed: true,
|
|
7988
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
7989
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
7990
|
+
},
|
|
7991
|
+
});
|
|
7992
|
+
const result = await dispatchCodexNativeHook({
|
|
7993
|
+
hook_event_name: "Stop",
|
|
7994
|
+
cwd,
|
|
7995
|
+
session_id: nativeSessionId,
|
|
7996
|
+
thread_id: "thread-B",
|
|
7997
|
+
}, { cwd });
|
|
7998
|
+
assert.equal(result.omxEventName, "stop");
|
|
7999
|
+
assert.deepEqual(result.outputJson, {
|
|
8000
|
+
decision: "block",
|
|
8001
|
+
reason: "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.",
|
|
8002
|
+
stopReason: "ralph_starting",
|
|
8003
|
+
systemMessage: "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.",
|
|
8004
|
+
});
|
|
8005
|
+
const preservedState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
8006
|
+
assert.equal(preservedState.active, true);
|
|
8007
|
+
assert.equal(preservedState.current_phase, "starting");
|
|
8008
|
+
assert.equal(preservedState.stop_reason, undefined);
|
|
8009
|
+
}
|
|
8010
|
+
finally {
|
|
8011
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8012
|
+
}
|
|
8013
|
+
});
|
|
6643
8014
|
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
6644
8015
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
6645
8016
|
try {
|