oh-my-codex 0.13.2 → 0.14.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 +5 -5
- package/Cargo.toml +1 -1
- package/README.md +14 -8
- package/crates/omx-explore/src/main.rs +94 -1
- package/crates/omx-sparkshell/src/codex_bridge.rs +59 -12
- package/crates/omx-sparkshell/tests/execution.rs +48 -0
- package/dist/autoresearch/__tests__/skill-validation.test.d.ts +2 -0
- package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +1 -0
- package/dist/autoresearch/__tests__/skill-validation.test.js +91 -0
- package/dist/autoresearch/__tests__/skill-validation.test.js.map +1 -0
- package/dist/autoresearch/skill-validation.d.ts +13 -0
- package/dist/autoresearch/skill-validation.d.ts.map +1 -0
- package/dist/autoresearch/skill-validation.js +165 -0
- package/dist/autoresearch/skill-validation.js.map +1 -0
- package/dist/catalog/__tests__/schema.test.js +6 -0
- package/dist/catalog/__tests__/schema.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js +236 -273
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js +64 -653
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +33 -1
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +18 -2
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +2 -1
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +5 -0
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.d.ts +2 -0
- package/dist/cli/__tests__/question.test.d.ts.map +1 -0
- package/dist/cli/__tests__/question.test.js +166 -0
- package/dist/cli/__tests__/question.test.js.map +1 -0
- package/dist/cli/__tests__/session-search-help.test.js +1 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +32 -7
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +8 -6
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -0
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +23 -0
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +65 -5
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +360 -26
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/autoresearch-guided.d.ts +24 -7
- package/dist/cli/autoresearch-guided.d.ts.map +1 -1
- package/dist/cli/autoresearch-guided.js +189 -130
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch.d.ts +3 -2
- package/dist/cli/autoresearch.d.ts.map +1 -1
- package/dist/cli/autoresearch.js +29 -305
- package/dist/cli/autoresearch.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +43 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +18 -3
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +15 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/question.d.ts +3 -0
- package/dist/cli/question.d.ts.map +1 -0
- package/dist/cli/question.js +182 -0
- package/dist/cli/question.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +25 -3
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +11 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +159 -394
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +3 -1
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts +37 -9
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +204 -26
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +51 -14
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +35 -10
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +1 -0
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +61 -7
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js +22 -13
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +3 -3
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +56 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +2 -2
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +51 -5
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js +43 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js +38 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +2 -2
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +308 -17
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +570 -2
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +717 -16
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +25 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +894 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +34 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +132 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +22 -4
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +4 -2
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +1 -0
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +19 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +28 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +5 -4
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-team-routing.test.js +2 -2
- package/dist/hooks/__tests__/prompt-team-routing.test.js.map +1 -1
- package/dist/hooks/__tests__/triage-config.test.d.ts +2 -0
- package/dist/hooks/__tests__/triage-config.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/triage-config.test.js +211 -0
- package/dist/hooks/__tests__/triage-config.test.js.map +1 -0
- package/dist/hooks/__tests__/triage-heuristic.test.d.ts +2 -0
- package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/triage-heuristic.test.js +230 -0
- package/dist/hooks/__tests__/triage-heuristic.test.js.map +1 -0
- package/dist/hooks/__tests__/triage-state.test.d.ts +2 -0
- package/dist/hooks/__tests__/triage-state.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/triage-state.test.js +426 -0
- package/dist/hooks/__tests__/triage-state.test.js.map +1 -0
- package/dist/hooks/keyword-detector.d.ts +26 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +97 -26
- 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 +16 -9
- 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 +28 -1
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/triage-config.d.ts +33 -0
- package/dist/hooks/triage-config.d.ts.map +1 -0
- package/dist/hooks/triage-config.js +87 -0
- package/dist/hooks/triage-config.js.map +1 -0
- package/dist/hooks/triage-heuristic.d.ts +20 -0
- package/dist/hooks/triage-heuristic.d.ts.map +1 -0
- package/dist/hooks/triage-heuristic.js +210 -0
- package/dist/hooks/triage-heuristic.js.map +1 -0
- package/dist/hooks/triage-state.d.ts +63 -0
- package/dist/hooks/triage-state.d.ts.map +1 -0
- package/dist/hooks/triage-state.js +138 -0
- package/dist/hooks/triage-state.js.map +1 -0
- package/dist/hud/__tests__/reconcile.test.js +20 -0
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/reconcile.d.ts +1 -0
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +2 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +5 -24
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +127 -0
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +1 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +3 -11
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/state-server.d.ts +25 -0
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +41 -0
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/modes/__tests__/base-ralph-contract.test.js +15 -0
- package/dist/modes/__tests__/base-ralph-contract.test.js.map +1 -1
- package/dist/modes/base.d.ts +1 -0
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +22 -6
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/index.test.js +75 -0
- package/dist/notifications/__tests__/index.test.js.map +1 -1
- package/dist/notifications/__tests__/session-status.test.js +90 -0
- package/dist/notifications/__tests__/session-status.test.js.map +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +39 -22
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/session-status.d.ts +2 -0
- package/dist/notifications/session-status.d.ts.map +1 -1
- package/dist/notifications/session-status.js +19 -4
- package/dist/notifications/session-status.js.map +1 -1
- package/dist/openclaw/index.d.ts +5 -3
- package/dist/openclaw/index.d.ts.map +1 -1
- package/dist/openclaw/index.js +5 -3
- package/dist/openclaw/index.js.map +1 -1
- package/dist/question/__tests__/client.test.d.ts +2 -0
- package/dist/question/__tests__/client.test.d.ts.map +1 -0
- package/dist/question/__tests__/client.test.js +70 -0
- package/dist/question/__tests__/client.test.js.map +1 -0
- package/dist/question/__tests__/deep-interview.test.d.ts +2 -0
- package/dist/question/__tests__/deep-interview.test.d.ts.map +1 -0
- package/dist/question/__tests__/deep-interview.test.js +118 -0
- package/dist/question/__tests__/deep-interview.test.js.map +1 -0
- package/dist/question/__tests__/policy.test.d.ts +2 -0
- package/dist/question/__tests__/policy.test.d.ts.map +1 -0
- package/dist/question/__tests__/policy.test.js +107 -0
- package/dist/question/__tests__/policy.test.js.map +1 -0
- package/dist/question/__tests__/renderer.test.d.ts +2 -0
- package/dist/question/__tests__/renderer.test.d.ts.map +1 -0
- package/dist/question/__tests__/renderer.test.js +238 -0
- package/dist/question/__tests__/renderer.test.js.map +1 -0
- package/dist/question/__tests__/state.test.d.ts +2 -0
- package/dist/question/__tests__/state.test.d.ts.map +1 -0
- package/dist/question/__tests__/state.test.js +75 -0
- package/dist/question/__tests__/state.test.js.map +1 -0
- package/dist/question/__tests__/types.test.d.ts +2 -0
- package/dist/question/__tests__/types.test.d.ts.map +1 -0
- package/dist/question/__tests__/types.test.js +44 -0
- package/dist/question/__tests__/types.test.js.map +1 -0
- package/dist/question/__tests__/ui.test.d.ts +2 -0
- package/dist/question/__tests__/ui.test.d.ts.map +1 -0
- package/dist/question/__tests__/ui.test.js +169 -0
- package/dist/question/__tests__/ui.test.js.map +1 -0
- package/dist/question/client.d.ts +54 -0
- package/dist/question/client.d.ts.map +1 -0
- package/dist/question/client.js +77 -0
- package/dist/question/client.js.map +1 -0
- package/dist/question/deep-interview.d.ts +30 -0
- package/dist/question/deep-interview.d.ts.map +1 -0
- package/dist/question/deep-interview.js +118 -0
- package/dist/question/deep-interview.js.map +1 -0
- package/dist/question/policy.d.ts +18 -0
- package/dist/question/policy.d.ts.map +1 -0
- package/dist/question/policy.js +77 -0
- package/dist/question/policy.js.map +1 -0
- package/dist/question/renderer.d.ts +20 -0
- package/dist/question/renderer.d.ts.map +1 -0
- package/dist/question/renderer.js +190 -0
- package/dist/question/renderer.js.map +1 -0
- package/dist/question/state.d.ts +19 -0
- package/dist/question/state.d.ts.map +1 -0
- package/dist/question/state.js +108 -0
- package/dist/question/state.js.map +1 -0
- package/dist/question/types.d.ts +66 -0
- package/dist/question/types.d.ts.map +1 -0
- package/dist/question/types.js +82 -0
- package/dist/question/types.js.map +1 -0
- package/dist/question/ui.d.ts +38 -0
- package/dist/question/ui.d.ts.map +1 -0
- package/dist/question/ui.js +321 -0
- package/dist/question/ui.js.map +1 -0
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralph/contract.js +4 -1
- package/dist/ralph/contract.js.map +1 -1
- package/dist/ralplan/runtime.js +1 -1
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/run-loop.test.d.ts +2 -0
- package/dist/runtime/__tests__/run-loop.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/run-loop.test.js +35 -0
- package/dist/runtime/__tests__/run-loop.test.js.map +1 -0
- package/dist/runtime/__tests__/run-outcome.test.d.ts +2 -0
- package/dist/runtime/__tests__/run-outcome.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/run-outcome.test.js +102 -0
- package/dist/runtime/__tests__/run-outcome.test.js.map +1 -0
- package/dist/runtime/__tests__/run-state.test.d.ts +2 -0
- package/dist/runtime/__tests__/run-state.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/run-state.test.js +37 -0
- package/dist/runtime/__tests__/run-state.test.js.map +1 -0
- package/dist/runtime/run-loop.d.ts +45 -0
- package/dist/runtime/run-loop.d.ts.map +1 -0
- package/dist/runtime/run-loop.js +51 -0
- package/dist/runtime/run-loop.js.map +1 -0
- package/dist/runtime/run-outcome.d.ts +46 -0
- package/dist/runtime/run-outcome.d.ts.map +1 -0
- package/dist/runtime/run-outcome.js +285 -0
- package/dist/runtime/run-outcome.js.map +1 -0
- package/dist/runtime/run-state.d.ts +40 -0
- package/dist/runtime/run-state.d.ts.map +1 -0
- package/dist/runtime/run-state.js +120 -0
- package/dist/runtime/run-state.js.map +1 -0
- package/dist/runtime/terminal-lifecycle.d.ts +11 -0
- package/dist/runtime/terminal-lifecycle.d.ts.map +1 -0
- package/dist/runtime/terminal-lifecycle.js +52 -0
- package/dist/runtime/terminal-lifecycle.js.map +1 -0
- package/dist/scripts/__tests__/codex-native-hook.test.js +1459 -126
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.d.ts +2 -0
- package/dist/scripts/__tests__/postinstall.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/postinstall.test.js +178 -0
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +3 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +308 -61
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +81 -2
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +27 -0
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +83 -20
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +64 -38
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook.js +15 -5
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.d.ts +22 -0
- package/dist/scripts/postinstall.d.ts.map +1 -0
- package/dist/scripts/postinstall.js +105 -0
- package/dist/scripts/postinstall.js.map +1 -0
- package/dist/scripts/sync-prompt-guidance-fragments.js +5 -0
- package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js +21 -0
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +18 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +11 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +15 -0
- package/dist/state/operations.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +14 -1
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/team/__tests__/followup-planner.test.js +15 -0
- package/dist/team/__tests__/followup-planner.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js +47 -0
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +108 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/followup-planner.d.ts.map +1 -1
- package/dist/team/followup-planner.js +31 -9
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/role-router.d.ts.map +1 -1
- package/dist/team/role-router.js +73 -0
- package/dist/team/role-router.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +18 -4
- package/dist/team/runtime.js.map +1 -1
- package/dist/utils/__tests__/dep-versions.test.js +25 -8
- package/dist/utils/__tests__/dep-versions.test.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +45 -0
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +22 -7
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +4 -2
- package/prompts/architect.md +4 -0
- package/prompts/code-reviewer.md +3 -0
- package/prompts/dependency-expert.md +3 -0
- package/prompts/executor.md +5 -0
- package/prompts/explore.md +2 -0
- package/prompts/planner.md +5 -0
- package/prompts/product-analyst.md +8 -8
- package/prompts/researcher.md +78 -30
- package/prompts/verifier.md +4 -0
- package/skills/autoresearch/SKILL.md +68 -0
- package/skills/code-review/SKILL.md +94 -28
- package/skills/deep-interview/SKILL.md +100 -9
- package/skills/help/SKILL.md +3 -1
- package/skills/ralplan/SKILL.md +1 -0
- package/skills/team/SKILL.md +1 -0
- package/skills/ultrawork/SKILL.md +1 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +2373 -692
- package/src/scripts/__tests__/postinstall.test.ts +210 -0
- package/src/scripts/codex-native-hook.ts +365 -66
- package/src/scripts/notify-fallback-watcher.ts +92 -2
- package/src/scripts/notify-hook/auto-nudge.ts +89 -20
- package/src/scripts/notify-hook/managed-tmux.ts +70 -31
- package/src/scripts/notify-hook/ralph-session-resume.ts +1 -1
- package/src/scripts/notify-hook.ts +23 -5
- package/src/scripts/postinstall-bootstrap.js +23 -0
- package/src/scripts/postinstall.ts +161 -0
- package/src/scripts/sync-prompt-guidance-fragments.ts +4 -0
- package/templates/AGENTS.md +48 -37
- package/templates/catalog-manifest.json +7 -0
- package/templates/model-instructions/explore-lightweight-AGENTS.md +11 -0
- package/templates/model-instructions/sparkshell-lightweight-AGENTS.md +10 -0
|
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
7
8
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
8
9
|
import { buildManagedCodexHooksConfig } from "../../config/codex-hooks.js";
|
|
9
10
|
import {
|
|
@@ -14,16 +15,42 @@ import {
|
|
|
14
15
|
} from "../../team/state.js";
|
|
15
16
|
import {
|
|
16
17
|
dispatchCodexNativeHook,
|
|
18
|
+
isCodexNativeHookMainModule,
|
|
17
19
|
mapCodexHookEventToOmxEvent,
|
|
18
20
|
resolveSessionOwnerPidFromAncestry,
|
|
19
21
|
} from "../codex-native-hook.js";
|
|
20
22
|
import { writeSessionStart } from "../../hooks/session.js";
|
|
23
|
+
import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
21
24
|
|
|
22
25
|
async function writeJson(path: string, value: unknown): Promise<void> {
|
|
23
26
|
await mkdir(dirname(path), { recursive: true }).catch(() => {});
|
|
24
27
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
async function writeHookCounterPlugin(cwd: string): Promise<string> {
|
|
31
|
+
const markerPath = join(cwd, ".omx", "stop-hook-counter.json");
|
|
32
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
33
|
+
await writeFile(
|
|
34
|
+
join(cwd, ".omx", "hooks", "count-stop-hook.mjs"),
|
|
35
|
+
`import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
36
|
+
import { dirname, join } from "node:path";
|
|
37
|
+
|
|
38
|
+
export async function onHookEvent(event) {
|
|
39
|
+
if (event.event !== "stop") return;
|
|
40
|
+
const outPath = join(process.cwd(), ".omx", "stop-hook-counter.json");
|
|
41
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
42
|
+
let count = 0;
|
|
43
|
+
try {
|
|
44
|
+
count = JSON.parse(await readFile(outPath, "utf-8")).count || 0;
|
|
45
|
+
} catch {}
|
|
46
|
+
await writeFile(outPath, JSON.stringify({ count: count + 1 }, null, 2));
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
"utf-8",
|
|
50
|
+
);
|
|
51
|
+
return markerPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
async function writeReleaseReadinessLeaderAttention(
|
|
28
55
|
teamName: string,
|
|
29
56
|
sessionId: string,
|
|
@@ -142,6 +169,25 @@ describe("codex native hook config", () => {
|
|
|
142
169
|
});
|
|
143
170
|
|
|
144
171
|
describe("codex native hook dispatch", () => {
|
|
172
|
+
it("treats space-containing argv entry paths as the main module", () => {
|
|
173
|
+
const entryPath = "/tmp/omx native/codex-native-hook.js";
|
|
174
|
+
|
|
175
|
+
assert.equal(
|
|
176
|
+
isCodexNativeHookMainModule(pathToFileURL(entryPath).href, entryPath),
|
|
177
|
+
true,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("does not treat a different module url as the main module", () => {
|
|
182
|
+
assert.equal(
|
|
183
|
+
isCodexNativeHookMainModule(
|
|
184
|
+
pathToFileURL("/tmp/omx native/other-script.js").href,
|
|
185
|
+
"/tmp/omx native/codex-native-hook.js",
|
|
186
|
+
),
|
|
187
|
+
false,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
145
191
|
it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
|
|
146
192
|
const stdout = execFileSync(
|
|
147
193
|
process.execPath,
|
|
@@ -262,7 +308,46 @@ describe("codex native hook dispatch", () => {
|
|
|
262
308
|
}
|
|
263
309
|
});
|
|
264
310
|
|
|
265
|
-
it("
|
|
311
|
+
it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
|
|
312
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
|
|
313
|
+
try {
|
|
314
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
315
|
+
const canonicalSessionId = "omx-launch-hud";
|
|
316
|
+
const nativeSessionId = "codex-native-hud";
|
|
317
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
318
|
+
await writeSessionStart(cwd, canonicalSessionId);
|
|
319
|
+
|
|
320
|
+
let reconcileCall: { cwd: string; sessionId?: string } | null = null;
|
|
321
|
+
const promptResult = await dispatchCodexNativeHook(
|
|
322
|
+
{
|
|
323
|
+
hook_event_name: "UserPromptSubmit",
|
|
324
|
+
cwd,
|
|
325
|
+
session_id: nativeSessionId,
|
|
326
|
+
thread_id: "thread-hud",
|
|
327
|
+
turn_id: "turn-hud",
|
|
328
|
+
prompt: "$ralplan fix orphaned hud session handoff",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
cwd,
|
|
332
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
333
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
|
|
334
|
+
return { status: 'recreated', paneId: '%9', desiredHeight: 3, duplicateCount: 0 };
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
340
|
+
assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
|
|
341
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), true);
|
|
342
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), true);
|
|
343
|
+
assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json")), false);
|
|
344
|
+
assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "ralplan-state.json")), false);
|
|
345
|
+
} finally {
|
|
346
|
+
await rm(cwd, { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("adds .omx/ to git info/exclude during SessionStart instead of mutating repo .gitignore", async () => {
|
|
266
351
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-gitignore-"));
|
|
267
352
|
try {
|
|
268
353
|
await writeFile(join(cwd, ".gitignore"), "node_modules/\n");
|
|
@@ -279,11 +364,68 @@ describe("codex native hook dispatch", () => {
|
|
|
279
364
|
|
|
280
365
|
assert.equal(result.omxEventName, "session-start");
|
|
281
366
|
const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
|
|
282
|
-
assert.
|
|
367
|
+
assert.equal(gitignore, "node_modules/\n");
|
|
368
|
+
const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
|
|
369
|
+
assert.match(exclude, /(?:^|\n)\.omx\/\n/);
|
|
283
370
|
assert.match(
|
|
284
371
|
JSON.stringify(result.outputJson),
|
|
285
|
-
/Added \.omx\/ to .*\.
|
|
372
|
+
/Added \.omx\/ to .*\.git[\/]info[\/]exclude/,
|
|
373
|
+
);
|
|
374
|
+
} finally {
|
|
375
|
+
await rm(cwd, { recursive: true, force: true });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("keeps SessionStart quiet when .omx/ is already ignored by repo-level gitignore", async () => {
|
|
380
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-existing-ignore-"));
|
|
381
|
+
try {
|
|
382
|
+
await writeFile(join(cwd, ".gitignore"), "node_modules/\n.omx/\n");
|
|
383
|
+
execFileSync("git", ["init"], { cwd, stdio: "pipe" });
|
|
384
|
+
|
|
385
|
+
const result = await dispatchCodexNativeHook(
|
|
386
|
+
{
|
|
387
|
+
hook_event_name: "SessionStart",
|
|
388
|
+
cwd,
|
|
389
|
+
session_id: "sess-gitignore-existing",
|
|
390
|
+
},
|
|
391
|
+
{ cwd, sessionOwnerPid: 43210 },
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
assert.equal(result.omxEventName, "session-start");
|
|
395
|
+
const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
|
|
396
|
+
assert.equal(gitignore, "node_modules/\n.omx/\n");
|
|
397
|
+
const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
|
|
398
|
+
assert.doesNotMatch(exclude, /(?:^|\n)\.omx\/\n/);
|
|
399
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /Added \.omx\//);
|
|
400
|
+
} finally {
|
|
401
|
+
await rm(cwd, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("respects existing Git ignore resolution before writing local excludes", async () => {
|
|
406
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-global-ignore-"));
|
|
407
|
+
const excludesFile = join(cwd, "global-ignore");
|
|
408
|
+
try {
|
|
409
|
+
await writeFile(join(cwd, ".gitignore"), "node_modules/\n");
|
|
410
|
+
await writeFile(excludesFile, ".omx/\n");
|
|
411
|
+
execFileSync("git", ["init"], { cwd, stdio: "pipe" });
|
|
412
|
+
execFileSync("git", ["config", "core.excludesfile", excludesFile], { cwd, stdio: "pipe" });
|
|
413
|
+
|
|
414
|
+
const result = await dispatchCodexNativeHook(
|
|
415
|
+
{
|
|
416
|
+
hook_event_name: "SessionStart",
|
|
417
|
+
cwd,
|
|
418
|
+
session_id: "sess-gitignore-global",
|
|
419
|
+
},
|
|
420
|
+
{ cwd, sessionOwnerPid: 43210 },
|
|
286
421
|
);
|
|
422
|
+
|
|
423
|
+
assert.equal(result.omxEventName, "session-start");
|
|
424
|
+
const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
|
|
425
|
+
assert.equal(gitignore, "node_modules/\n");
|
|
426
|
+
const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
|
|
427
|
+
assert.doesNotMatch(exclude, /(?:^|\n)\.omx\/\n/);
|
|
428
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /Added \.omx\//);
|
|
287
429
|
} finally {
|
|
288
430
|
await rm(cwd, { recursive: true, force: true });
|
|
289
431
|
}
|
|
@@ -482,7 +624,14 @@ describe("codex native hook dispatch", () => {
|
|
|
482
624
|
|
|
483
625
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
484
626
|
assert.equal(result.skillState, null);
|
|
485
|
-
|
|
627
|
+
// Triage may inject advisory LIGHT/explore context for the question-shaped
|
|
628
|
+
// prompt, but the invariant this test guards is that no Ralph workflow state
|
|
629
|
+
// is seeded and no Ralph-activation message is emitted.
|
|
630
|
+
const advisoryContext = String(
|
|
631
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
632
|
+
);
|
|
633
|
+
assert.doesNotMatch(advisoryContext, /skill:\s*ralph/i);
|
|
634
|
+
assert.doesNotMatch(advisoryContext, /ralph-state\.json/i);
|
|
486
635
|
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
487
636
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "skill-active-state.json")), false);
|
|
488
637
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "ralph-state.json")), false);
|
|
@@ -491,6 +640,67 @@ describe("codex native hook dispatch", () => {
|
|
|
491
640
|
}
|
|
492
641
|
});
|
|
493
642
|
|
|
643
|
+
it("adds execution handoff context for non-keyword prompts that authorize implementation", async () => {
|
|
644
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-execution-handoff-"));
|
|
645
|
+
try {
|
|
646
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
647
|
+
const prompts = [
|
|
648
|
+
"按照这个plan开始执行优化",
|
|
649
|
+
"开始执行",
|
|
650
|
+
"继续优化",
|
|
651
|
+
"直接修复",
|
|
652
|
+
];
|
|
653
|
+
|
|
654
|
+
for (const [index, prompt] of prompts.entries()) {
|
|
655
|
+
const result = await dispatchCodexNativeHook(
|
|
656
|
+
{
|
|
657
|
+
hook_event_name: "UserPromptSubmit",
|
|
658
|
+
cwd,
|
|
659
|
+
session_id: `sess-exec-handoff-${index}`,
|
|
660
|
+
thread_id: `thread-exec-handoff-${index}`,
|
|
661
|
+
turn_id: `turn-exec-handoff-${index}`,
|
|
662
|
+
prompt,
|
|
663
|
+
},
|
|
664
|
+
{ cwd },
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const message = String(
|
|
668
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
669
|
+
);
|
|
670
|
+
assert.match(message, /execution handoff/i, prompt);
|
|
671
|
+
assert.match(message, /Do not restate the prior plan/i, prompt);
|
|
672
|
+
}
|
|
673
|
+
} finally {
|
|
674
|
+
await rm(cwd, { recursive: true, force: true });
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("adds latest-followup priority context for short same-thread follow-up prompts", async () => {
|
|
679
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-followup-priority-"));
|
|
680
|
+
try {
|
|
681
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
682
|
+
const result = await dispatchCodexNativeHook(
|
|
683
|
+
{
|
|
684
|
+
hook_event_name: "UserPromptSubmit",
|
|
685
|
+
cwd,
|
|
686
|
+
session_id: "sess-followup-priority",
|
|
687
|
+
thread_id: "thread-followup-priority",
|
|
688
|
+
turn_id: "turn-followup-priority",
|
|
689
|
+
prompt: "这些优化都做了么",
|
|
690
|
+
},
|
|
691
|
+
{ cwd },
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const message = String(
|
|
695
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
696
|
+
);
|
|
697
|
+
assert.match(message, /same-thread follow-up/i);
|
|
698
|
+
assert.match(message, /prefer it over older unresolved prompts/i);
|
|
699
|
+
} finally {
|
|
700
|
+
await rm(cwd, { recursive: true, force: true });
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
494
704
|
it("clarifies that prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
|
|
495
705
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-routing-"));
|
|
496
706
|
try {
|
|
@@ -521,6 +731,146 @@ describe("codex native hook dispatch", () => {
|
|
|
521
731
|
}
|
|
522
732
|
});
|
|
523
733
|
|
|
734
|
+
it("keeps bare keep-going continuation on the active autopilot skill instead of denying with generic ralph overlap", async () => {
|
|
735
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-bare-continuation-"));
|
|
736
|
+
try {
|
|
737
|
+
const sessionId = "sess-autopilot-cont";
|
|
738
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
739
|
+
await mkdir(sessionDir, { recursive: true });
|
|
740
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
741
|
+
version: 1,
|
|
742
|
+
active: true,
|
|
743
|
+
skill: "autopilot",
|
|
744
|
+
keyword: "$autopilot",
|
|
745
|
+
phase: "planning",
|
|
746
|
+
session_id: sessionId,
|
|
747
|
+
active_skills: [
|
|
748
|
+
{ skill: "autopilot", phase: "planning", active: true, session_id: sessionId },
|
|
749
|
+
],
|
|
750
|
+
});
|
|
751
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
752
|
+
active: true,
|
|
753
|
+
mode: "autopilot",
|
|
754
|
+
current_phase: "execution",
|
|
755
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
756
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
757
|
+
session_id: sessionId,
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const result = await dispatchCodexNativeHook(
|
|
761
|
+
{
|
|
762
|
+
hook_event_name: "UserPromptSubmit",
|
|
763
|
+
cwd,
|
|
764
|
+
session_id: sessionId,
|
|
765
|
+
thread_id: "thread-autopilot-cont",
|
|
766
|
+
turn_id: "turn-autopilot-cont",
|
|
767
|
+
prompt: "\ keep going now",
|
|
768
|
+
},
|
|
769
|
+
{ cwd },
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
773
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
774
|
+
const message = String(
|
|
775
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
776
|
+
);
|
|
777
|
+
assert.match(message, /"keep going" -> ralph/);
|
|
778
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
779
|
+
assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
|
|
780
|
+
assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
|
|
781
|
+
assert.equal(existsSync(join(sessionDir, "ralph-state.json")), false);
|
|
782
|
+
} finally {
|
|
783
|
+
await rm(cwd, { recursive: true, force: true });
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it("clarifies that prompt-side deep-interview activation must use omx question", async () => {
|
|
788
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
|
|
789
|
+
try {
|
|
790
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
791
|
+
const result = await dispatchCodexNativeHook(
|
|
792
|
+
{
|
|
793
|
+
hook_event_name: "UserPromptSubmit",
|
|
794
|
+
cwd,
|
|
795
|
+
session_id: "sess-deep-interview-msg",
|
|
796
|
+
thread_id: "thread-deep-interview-msg",
|
|
797
|
+
turn_id: "turn-deep-interview-msg",
|
|
798
|
+
prompt: "$deep-interview gather requirements",
|
|
799
|
+
},
|
|
800
|
+
{ cwd },
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
804
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
805
|
+
const message = String(
|
|
806
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
807
|
+
);
|
|
808
|
+
assert.match(message, /\$deep-interview" -> deep-interview/);
|
|
809
|
+
assert.match(message, /skill: deep-interview activated and initial state initialized at \.omx\/state\/sessions\/sess-deep-interview-msg\/deep-interview-state\.json; write subsequent updates via omx_state MCP\./);
|
|
810
|
+
assert.match(message, /Deep-interview must ask each interview round via `omx question`/);
|
|
811
|
+
assert.match(message, /do not fall back to `request_user_input` or plain-text questioning/i);
|
|
812
|
+
assert.match(message, /If bare `omx question` is unavailable in this reused session, use the current-session CLI bridge command:/);
|
|
813
|
+
assert.match(message, /`'.+' '.+dist\/cli\/omx\.js' question`/);
|
|
814
|
+
assert.match(message, /Stop remains blocked while a deep-interview question obligation is pending\./);
|
|
815
|
+
} finally {
|
|
816
|
+
await rm(cwd, { recursive: true, force: true });
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("keeps bare keep-going continuation on the active ralph skill without resetting through generic keep-going routing", async () => {
|
|
821
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-bare-continuation-"));
|
|
822
|
+
try {
|
|
823
|
+
const sessionId = "sess-ralph-cont";
|
|
824
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
825
|
+
await mkdir(sessionDir, { recursive: true });
|
|
826
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
827
|
+
version: 1,
|
|
828
|
+
active: true,
|
|
829
|
+
skill: "ralph",
|
|
830
|
+
keyword: "$ralph",
|
|
831
|
+
phase: "executing",
|
|
832
|
+
session_id: sessionId,
|
|
833
|
+
active_skills: [
|
|
834
|
+
{ skill: "ralph", phase: "executing", active: true, session_id: sessionId },
|
|
835
|
+
],
|
|
836
|
+
});
|
|
837
|
+
await writeJson(join(sessionDir, "ralph-state.json"), {
|
|
838
|
+
active: true,
|
|
839
|
+
mode: "ralph",
|
|
840
|
+
current_phase: "verifying",
|
|
841
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
842
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
843
|
+
iteration: 4,
|
|
844
|
+
max_iterations: 50,
|
|
845
|
+
session_id: sessionId,
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const result = await dispatchCodexNativeHook(
|
|
849
|
+
{
|
|
850
|
+
hook_event_name: "UserPromptSubmit",
|
|
851
|
+
cwd,
|
|
852
|
+
session_id: sessionId,
|
|
853
|
+
thread_id: "thread-ralph-cont",
|
|
854
|
+
turn_id: "turn-ralph-cont",
|
|
855
|
+
prompt: "keep going now",
|
|
856
|
+
},
|
|
857
|
+
{ cwd },
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
861
|
+
assert.equal(result.skillState?.skill, "ralph");
|
|
862
|
+
const message = String(
|
|
863
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
864
|
+
);
|
|
865
|
+
assert.match(message, /"keep going" -> ralph/);
|
|
866
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
867
|
+
assert.doesNotMatch(message, /mode transiting:/);
|
|
868
|
+
} finally {
|
|
869
|
+
await rm(cwd, { recursive: true, force: true });
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
|
|
524
874
|
it("ignores generic wrapper fields so metadata cannot trigger workflow routing or Stop blocking", async () => {
|
|
525
875
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-wrapper-metadata-"));
|
|
526
876
|
try {
|
|
@@ -1942,6 +2292,33 @@ esac
|
|
|
1942
2292
|
}
|
|
1943
2293
|
});
|
|
1944
2294
|
|
|
2295
|
+
it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
|
|
2296
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
|
|
2297
|
+
try {
|
|
2298
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2299
|
+
await mkdir(stateDir, { recursive: true });
|
|
2300
|
+
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
2301
|
+
active: true,
|
|
2302
|
+
current_phase: "execution",
|
|
2303
|
+
run_outcome: "blocked_on_user",
|
|
2304
|
+
});
|
|
2305
|
+
|
|
2306
|
+
const result = await dispatchCodexNativeHook(
|
|
2307
|
+
{
|
|
2308
|
+
hook_event_name: "Stop",
|
|
2309
|
+
cwd,
|
|
2310
|
+
session_id: "sess-stop-autopilot-blocked-outcome",
|
|
2311
|
+
},
|
|
2312
|
+
{ cwd },
|
|
2313
|
+
);
|
|
2314
|
+
|
|
2315
|
+
assert.equal(result.omxEventName, "stop");
|
|
2316
|
+
assert.equal(result.outputJson, null);
|
|
2317
|
+
} finally {
|
|
2318
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
|
|
1945
2322
|
it("returns Stop continuation output while Ultrawork is active", async () => {
|
|
1946
2323
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
|
|
1947
2324
|
try {
|
|
@@ -2699,201 +3076,371 @@ esac
|
|
|
2699
3076
|
}
|
|
2700
3077
|
});
|
|
2701
3078
|
|
|
2702
|
-
it("
|
|
2703
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3079
|
+
it("blocks Stop while autoresearch is active without validator completion", async () => {
|
|
3080
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-"));
|
|
2704
3081
|
try {
|
|
2705
3082
|
const stateDir = join(cwd, ".omx", "state");
|
|
2706
|
-
await mkdir(join(stateDir, "sessions", "sess-stop-
|
|
2707
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-
|
|
2708
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-
|
|
2709
|
-
active: true,
|
|
2710
|
-
skill: "deep-interview",
|
|
2711
|
-
phase: "planning",
|
|
2712
|
-
});
|
|
2713
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
|
|
3083
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch"), { recursive: true });
|
|
3084
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch", cwd });
|
|
3085
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch", "autoresearch-state.json"), {
|
|
2714
3086
|
active: true,
|
|
2715
|
-
|
|
3087
|
+
mode: "autoresearch",
|
|
3088
|
+
current_phase: "executing",
|
|
3089
|
+
session_id: "sess-stop-autoresearch",
|
|
3090
|
+
validation_mode: "mission-validator-script",
|
|
3091
|
+
mission_validator_command: "node scripts/validate.js",
|
|
3092
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
2716
3093
|
});
|
|
2717
3094
|
|
|
2718
3095
|
const result = await dispatchCodexNativeHook(
|
|
2719
3096
|
{
|
|
2720
3097
|
hook_event_name: "Stop",
|
|
2721
3098
|
cwd,
|
|
2722
|
-
session_id: "sess-stop-
|
|
3099
|
+
session_id: "sess-stop-autoresearch",
|
|
2723
3100
|
},
|
|
2724
3101
|
{ cwd },
|
|
2725
3102
|
);
|
|
2726
3103
|
|
|
2727
|
-
assert.equal(result.
|
|
3104
|
+
assert.equal(result.omxEventName, "stop");
|
|
3105
|
+
assert.deepEqual(result.outputJson, {
|
|
3106
|
+
decision: "block",
|
|
3107
|
+
reason: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
|
|
3108
|
+
stopReason: "autoresearch_executing",
|
|
3109
|
+
systemMessage: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
|
|
3110
|
+
});
|
|
2728
3111
|
} finally {
|
|
2729
3112
|
await rm(cwd, { recursive: true, force: true });
|
|
2730
3113
|
}
|
|
2731
3114
|
});
|
|
2732
3115
|
|
|
2733
|
-
it("
|
|
2734
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3116
|
+
it("allows Stop once autoresearch validator evidence is complete", async () => {
|
|
3117
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-complete-"));
|
|
2735
3118
|
try {
|
|
2736
3119
|
const stateDir = join(cwd, ".omx", "state");
|
|
2737
|
-
|
|
2738
|
-
await
|
|
3120
|
+
const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
|
|
3121
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch-complete"), { recursive: true });
|
|
3122
|
+
await mkdir(specDir, { recursive: true });
|
|
3123
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch-complete", cwd });
|
|
3124
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch-complete", "autoresearch-state.json"), {
|
|
2739
3125
|
active: true,
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
session_id: "",
|
|
2743
|
-
|
|
3126
|
+
mode: "autoresearch",
|
|
3127
|
+
current_phase: "reviewing",
|
|
3128
|
+
session_id: "sess-stop-autoresearch-complete",
|
|
3129
|
+
validation_mode: "mission-validator-script",
|
|
3130
|
+
mission_validator_command: "node scripts/validate.js",
|
|
3131
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
2744
3132
|
});
|
|
3133
|
+
await writeJson(join(specDir, 'completion.json'), { status: 'passed', passed: true });
|
|
2745
3134
|
|
|
2746
3135
|
const result = await dispatchCodexNativeHook(
|
|
2747
3136
|
{
|
|
2748
3137
|
hook_event_name: "Stop",
|
|
2749
3138
|
cwd,
|
|
2750
|
-
session_id: "sess-stop-
|
|
2751
|
-
thread_id: "main-thread",
|
|
3139
|
+
session_id: "sess-stop-autoresearch-complete",
|
|
2752
3140
|
},
|
|
2753
3141
|
{ cwd },
|
|
2754
3142
|
);
|
|
2755
3143
|
|
|
3144
|
+
assert.equal(result.omxEventName, "stop");
|
|
2756
3145
|
assert.equal(result.outputJson, null);
|
|
2757
3146
|
} finally {
|
|
2758
3147
|
await rm(cwd, { recursive: true, force: true });
|
|
2759
3148
|
}
|
|
2760
3149
|
});
|
|
2761
3150
|
|
|
2762
|
-
it("
|
|
2763
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
|
|
3151
|
+
it("does not block Stop from stale root autoresearch state when the explicit session has no scoped autoresearch state", async () => {
|
|
3152
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autoresearch-"));
|
|
2764
3153
|
try {
|
|
2765
3154
|
const stateDir = join(cwd, ".omx", "state");
|
|
2766
|
-
|
|
2767
|
-
await
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
3155
|
+
const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
|
|
3156
|
+
await mkdir(join(stateDir, 'sessions', 'sess-current'), { recursive: true });
|
|
3157
|
+
await mkdir(specDir, { recursive: true });
|
|
3158
|
+
await writeJson(join(stateDir, 'session.json'), { session_id: 'sess-current', cwd });
|
|
3159
|
+
await writeJson(join(stateDir, 'autoresearch-state.json'), {
|
|
3160
|
+
active: true,
|
|
3161
|
+
mode: 'autoresearch',
|
|
3162
|
+
current_phase: 'executing',
|
|
3163
|
+
validation_mode: 'mission-validator-script',
|
|
3164
|
+
mission_validator_command: 'node scripts/validate.js',
|
|
3165
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
3166
|
+
});
|
|
2774
3167
|
|
|
2775
3168
|
const result = await dispatchCodexNativeHook(
|
|
2776
3169
|
{
|
|
2777
|
-
hook_event_name:
|
|
3170
|
+
hook_event_name: 'Stop',
|
|
2778
3171
|
cwd,
|
|
3172
|
+
session_id: 'sess-current',
|
|
2779
3173
|
},
|
|
2780
3174
|
{ cwd },
|
|
2781
3175
|
);
|
|
2782
3176
|
|
|
2783
|
-
assert.equal(result.omxEventName,
|
|
2784
|
-
assert.
|
|
2785
|
-
decision: "block",
|
|
2786
|
-
reason:
|
|
2787
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2788
|
-
stopReason: "ralph_executing",
|
|
2789
|
-
systemMessage:
|
|
2790
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2791
|
-
});
|
|
3177
|
+
assert.equal(result.omxEventName, 'stop');
|
|
3178
|
+
assert.equal(result.outputJson, null);
|
|
2792
3179
|
} finally {
|
|
2793
3180
|
await rm(cwd, { recursive: true, force: true });
|
|
2794
3181
|
}
|
|
2795
3182
|
});
|
|
2796
3183
|
|
|
2797
|
-
it("
|
|
2798
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3184
|
+
it("does not block Stop solely because deep-interview is active", async () => {
|
|
3185
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
|
|
2799
3186
|
try {
|
|
2800
3187
|
const stateDir = join(cwd, ".omx", "state");
|
|
2801
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2802
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-
|
|
2803
|
-
await writeJson(join(stateDir, "sessions", "sess-
|
|
3188
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview"), { recursive: true });
|
|
3189
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview" });
|
|
3190
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "skill-active-state.json"), {
|
|
2804
3191
|
active: true,
|
|
2805
|
-
|
|
2806
|
-
|
|
3192
|
+
skill: "deep-interview",
|
|
3193
|
+
phase: "planning",
|
|
3194
|
+
});
|
|
3195
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
|
|
3196
|
+
active: true,
|
|
3197
|
+
current_phase: "planning",
|
|
2807
3198
|
});
|
|
2808
3199
|
|
|
2809
3200
|
const result = await dispatchCodexNativeHook(
|
|
2810
3201
|
{
|
|
2811
3202
|
hook_event_name: "Stop",
|
|
2812
3203
|
cwd,
|
|
2813
|
-
session_id: "sess-
|
|
3204
|
+
session_id: "sess-stop-deep-interview",
|
|
2814
3205
|
},
|
|
2815
3206
|
{ cwd },
|
|
2816
3207
|
);
|
|
2817
3208
|
|
|
2818
|
-
assert.equal(result.
|
|
2819
|
-
assert.deepEqual(result.outputJson, {
|
|
2820
|
-
decision: "block",
|
|
2821
|
-
reason:
|
|
2822
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2823
|
-
stopReason: "ralph_executing",
|
|
2824
|
-
systemMessage:
|
|
2825
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2826
|
-
});
|
|
3209
|
+
assert.equal(result.outputJson, null);
|
|
2827
3210
|
} finally {
|
|
2828
3211
|
await rm(cwd, { recursive: true, force: true });
|
|
2829
3212
|
}
|
|
2830
3213
|
});
|
|
2831
3214
|
|
|
2832
|
-
it("
|
|
2833
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3215
|
+
it("blocks Stop when deep-interview has a pending omx question obligation", async () => {
|
|
3216
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-"));
|
|
2834
3217
|
try {
|
|
2835
3218
|
const stateDir = join(cwd, ".omx", "state");
|
|
2836
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2837
|
-
await
|
|
2838
|
-
await writeJson(join(stateDir, "
|
|
2839
|
-
|
|
3219
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question"), { recursive: true });
|
|
3220
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question" });
|
|
3221
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "skill-active-state.json"), {
|
|
3222
|
+
version: 1,
|
|
2840
3223
|
active: true,
|
|
2841
|
-
|
|
2842
|
-
|
|
3224
|
+
skill: "deep-interview",
|
|
3225
|
+
phase: "planning",
|
|
3226
|
+
session_id: "sess-stop-deep-interview-question",
|
|
3227
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
3228
|
+
});
|
|
3229
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "deep-interview-state.json"), {
|
|
3230
|
+
active: true,
|
|
3231
|
+
mode: "deep-interview",
|
|
3232
|
+
current_phase: "intent-first",
|
|
3233
|
+
session_id: "sess-stop-deep-interview-question",
|
|
3234
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
3235
|
+
question_enforcement: {
|
|
3236
|
+
obligation_id: "obligation-1",
|
|
3237
|
+
source: "omx-question",
|
|
3238
|
+
status: "pending",
|
|
3239
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3240
|
+
},
|
|
2843
3241
|
});
|
|
2844
3242
|
|
|
2845
3243
|
const result = await dispatchCodexNativeHook(
|
|
2846
3244
|
{
|
|
2847
3245
|
hook_event_name: "Stop",
|
|
2848
3246
|
cwd,
|
|
2849
|
-
session_id: "sess-
|
|
3247
|
+
session_id: "sess-stop-deep-interview-question",
|
|
3248
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
2850
3249
|
},
|
|
2851
3250
|
{ cwd },
|
|
2852
3251
|
);
|
|
2853
3252
|
|
|
2854
3253
|
assert.equal(result.omxEventName, "stop");
|
|
2855
|
-
assert.
|
|
3254
|
+
assert.deepEqual(result.outputJson, {
|
|
3255
|
+
decision: "block",
|
|
3256
|
+
reason:
|
|
3257
|
+
"Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
3258
|
+
stopReason: "deep_interview_question_required",
|
|
3259
|
+
systemMessage:
|
|
3260
|
+
"OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
3261
|
+
});
|
|
2856
3262
|
} finally {
|
|
2857
3263
|
await rm(cwd, { recursive: true, force: true });
|
|
2858
3264
|
}
|
|
2859
3265
|
});
|
|
2860
3266
|
|
|
2861
|
-
it("
|
|
2862
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3267
|
+
it("blocks Stop when a same-session deep-interview question obligation is pending even after the mode marked itself inactive", async () => {
|
|
3268
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-inactive-"));
|
|
2863
3269
|
try {
|
|
2864
3270
|
const stateDir = join(cwd, ".omx", "state");
|
|
2865
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2866
|
-
await writeJson(join(stateDir, "
|
|
3271
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive"), { recursive: true });
|
|
3272
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-inactive" });
|
|
3273
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive", "skill-active-state.json"), {
|
|
3274
|
+
version: 1,
|
|
2867
3275
|
active: true,
|
|
2868
|
-
|
|
2869
|
-
|
|
3276
|
+
skill: "deep-interview",
|
|
3277
|
+
phase: "planning",
|
|
3278
|
+
session_id: "sess-stop-deep-interview-question-inactive",
|
|
3279
|
+
thread_id: "thread-stop-deep-interview-question-inactive",
|
|
3280
|
+
});
|
|
3281
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive", "deep-interview-state.json"), {
|
|
3282
|
+
active: false,
|
|
3283
|
+
mode: "deep-interview",
|
|
3284
|
+
current_phase: "intent-first",
|
|
3285
|
+
lifecycle_outcome: "askuserQuestion",
|
|
3286
|
+
run_outcome: "blocked_on_user",
|
|
3287
|
+
completed_at: "2026-04-19T03:20:30.000Z",
|
|
3288
|
+
session_id: "sess-stop-deep-interview-question-inactive",
|
|
3289
|
+
thread_id: "thread-stop-deep-interview-question-inactive",
|
|
3290
|
+
question_enforcement: {
|
|
3291
|
+
obligation_id: "obligation-inactive",
|
|
3292
|
+
source: "omx-question",
|
|
3293
|
+
status: "pending",
|
|
3294
|
+
lifecycle_outcome: "askuserQuestion",
|
|
3295
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3296
|
+
},
|
|
2870
3297
|
});
|
|
2871
3298
|
|
|
2872
3299
|
const result = await dispatchCodexNativeHook(
|
|
2873
3300
|
{
|
|
2874
3301
|
hook_event_name: "Stop",
|
|
2875
3302
|
cwd,
|
|
2876
|
-
session_id: "sess-
|
|
3303
|
+
session_id: "sess-stop-deep-interview-question-inactive",
|
|
3304
|
+
thread_id: "thread-stop-deep-interview-question-inactive",
|
|
2877
3305
|
},
|
|
2878
3306
|
{ cwd },
|
|
2879
3307
|
);
|
|
2880
3308
|
|
|
2881
3309
|
assert.equal(result.omxEventName, "stop");
|
|
2882
|
-
assert.
|
|
3310
|
+
assert.deepEqual(result.outputJson, {
|
|
3311
|
+
decision: "block",
|
|
3312
|
+
reason:
|
|
3313
|
+
"Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
3314
|
+
stopReason: "deep_interview_question_required",
|
|
3315
|
+
systemMessage:
|
|
3316
|
+
"OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
3317
|
+
});
|
|
2883
3318
|
} finally {
|
|
2884
3319
|
await rm(cwd, { recursive: true, force: true });
|
|
2885
3320
|
}
|
|
2886
3321
|
});
|
|
2887
3322
|
|
|
2888
|
-
it("
|
|
2889
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3323
|
+
it("keeps blocking pending deep-interview question Stop replays until the obligation changes", async () => {
|
|
3324
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-replay-"));
|
|
3325
|
+
try {
|
|
3326
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3327
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay"), { recursive: true });
|
|
3328
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-replay" });
|
|
3329
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "skill-active-state.json"), {
|
|
3330
|
+
version: 1,
|
|
3331
|
+
active: true,
|
|
3332
|
+
skill: "deep-interview",
|
|
3333
|
+
phase: "planning",
|
|
3334
|
+
session_id: "sess-stop-deep-interview-question-replay",
|
|
3335
|
+
});
|
|
3336
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "deep-interview-state.json"), {
|
|
3337
|
+
active: true,
|
|
3338
|
+
mode: "deep-interview",
|
|
3339
|
+
current_phase: "intent-first",
|
|
3340
|
+
question_enforcement: {
|
|
3341
|
+
obligation_id: "obligation-replay",
|
|
3342
|
+
source: "omx-question",
|
|
3343
|
+
status: "pending",
|
|
3344
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3345
|
+
},
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
const payload = {
|
|
3349
|
+
hook_event_name: "Stop",
|
|
3350
|
+
cwd,
|
|
3351
|
+
session_id: "sess-stop-deep-interview-question-replay",
|
|
3352
|
+
};
|
|
3353
|
+
const expected = {
|
|
3354
|
+
decision: "block",
|
|
3355
|
+
reason:
|
|
3356
|
+
"Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
3357
|
+
stopReason: "deep_interview_question_required",
|
|
3358
|
+
systemMessage:
|
|
3359
|
+
"OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
3360
|
+
};
|
|
3361
|
+
|
|
3362
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
3363
|
+
const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
|
|
3364
|
+
|
|
3365
|
+
assert.equal(first.omxEventName, "stop");
|
|
3366
|
+
assert.deepEqual(first.outputJson, expected);
|
|
3367
|
+
assert.equal(replay.omxEventName, "stop");
|
|
3368
|
+
assert.deepEqual(replay.outputJson, expected);
|
|
3369
|
+
} finally {
|
|
3370
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
it("does not block Stop once the deep-interview question obligation is satisfied or cleared", async () => {
|
|
3375
|
+
for (const status of ["satisfied", "cleared"] as const) {
|
|
3376
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-deep-interview-question-${status}-`));
|
|
3377
|
+
try {
|
|
3378
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3379
|
+
await mkdir(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`), { recursive: true });
|
|
3380
|
+
await writeJson(join(stateDir, "session.json"), { session_id: `sess-stop-deep-interview-question-${status}` });
|
|
3381
|
+
await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "skill-active-state.json"), {
|
|
3382
|
+
version: 1,
|
|
3383
|
+
active: true,
|
|
3384
|
+
skill: "deep-interview",
|
|
3385
|
+
phase: "planning",
|
|
3386
|
+
session_id: `sess-stop-deep-interview-question-${status}`,
|
|
3387
|
+
});
|
|
3388
|
+
await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "deep-interview-state.json"), {
|
|
3389
|
+
active: true,
|
|
3390
|
+
mode: "deep-interview",
|
|
3391
|
+
current_phase: "intent-first",
|
|
3392
|
+
question_enforcement: {
|
|
3393
|
+
obligation_id: `obligation-${status}`,
|
|
3394
|
+
source: "omx-question",
|
|
3395
|
+
status,
|
|
3396
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3397
|
+
...(status === "satisfied"
|
|
3398
|
+
? { question_id: "question-1", satisfied_at: "2026-04-19T03:21:00.000Z" }
|
|
3399
|
+
: { cleared_at: "2026-04-19T03:21:00.000Z", clear_reason: "error" }),
|
|
3400
|
+
},
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
const result = await dispatchCodexNativeHook(
|
|
3404
|
+
{
|
|
3405
|
+
hook_event_name: "Stop",
|
|
3406
|
+
cwd,
|
|
3407
|
+
session_id: `sess-stop-deep-interview-question-${status}`,
|
|
3408
|
+
},
|
|
3409
|
+
{ cwd },
|
|
3410
|
+
);
|
|
3411
|
+
|
|
3412
|
+
assert.equal(result.omxEventName, "stop");
|
|
3413
|
+
assert.equal(result.outputJson, null);
|
|
3414
|
+
} finally {
|
|
3415
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
|
|
3420
|
+
it("ignores pending deep-interview question obligations from another session", async () => {
|
|
3421
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-foreign-session-"));
|
|
2890
3422
|
try {
|
|
2891
3423
|
const stateDir = join(cwd, ".omx", "state");
|
|
3424
|
+
await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
|
|
2892
3425
|
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
2893
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current"
|
|
2894
|
-
await writeJson(join(stateDir, "
|
|
3426
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
3427
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "skill-active-state.json"), {
|
|
3428
|
+
version: 1,
|
|
2895
3429
|
active: true,
|
|
2896
|
-
|
|
3430
|
+
skill: "deep-interview",
|
|
3431
|
+
phase: "planning",
|
|
3432
|
+
session_id: "sess-other",
|
|
3433
|
+
});
|
|
3434
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "deep-interview-state.json"), {
|
|
3435
|
+
active: true,
|
|
3436
|
+
mode: "deep-interview",
|
|
3437
|
+
current_phase: "intent-first",
|
|
3438
|
+
question_enforcement: {
|
|
3439
|
+
obligation_id: "obligation-foreign",
|
|
3440
|
+
source: "omx-question",
|
|
3441
|
+
status: "pending",
|
|
3442
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3443
|
+
},
|
|
2897
3444
|
});
|
|
2898
3445
|
|
|
2899
3446
|
const result = await dispatchCodexNativeHook(
|
|
@@ -2912,72 +3459,87 @@ esac
|
|
|
2912
3459
|
}
|
|
2913
3460
|
});
|
|
2914
3461
|
|
|
2915
|
-
it("
|
|
2916
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3462
|
+
it("blocks a new same-session deep-interview question obligation even after an earlier round was satisfied", async () => {
|
|
3463
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-next-round-"));
|
|
2917
3464
|
try {
|
|
2918
3465
|
const stateDir = join(cwd, ".omx", "state");
|
|
2919
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2920
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-
|
|
2921
|
-
await writeJson(join(stateDir, "sessions", "sess-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3466
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round"), { recursive: true });
|
|
3467
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-next-round" });
|
|
3468
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "skill-active-state.json"), {
|
|
3469
|
+
version: 1,
|
|
3470
|
+
active: true,
|
|
3471
|
+
skill: "deep-interview",
|
|
3472
|
+
phase: "planning",
|
|
3473
|
+
session_id: "sess-stop-deep-interview-question-next-round",
|
|
2926
3474
|
});
|
|
2927
|
-
await writeJson(join(stateDir, "
|
|
3475
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "deep-interview-state.json"), {
|
|
2928
3476
|
active: true,
|
|
2929
|
-
|
|
3477
|
+
mode: "deep-interview",
|
|
3478
|
+
current_phase: "intent-first",
|
|
3479
|
+
question_enforcement: {
|
|
3480
|
+
obligation_id: "obligation-next-round",
|
|
3481
|
+
source: "omx-question",
|
|
3482
|
+
status: "pending",
|
|
3483
|
+
requested_at: "2026-04-19T03:22:00.000Z",
|
|
3484
|
+
question_id: "question-old-round",
|
|
3485
|
+
satisfied_at: "2026-04-19T03:21:00.000Z",
|
|
3486
|
+
},
|
|
2930
3487
|
});
|
|
2931
3488
|
|
|
2932
3489
|
const result = await dispatchCodexNativeHook(
|
|
2933
3490
|
{
|
|
2934
3491
|
hook_event_name: "Stop",
|
|
2935
3492
|
cwd,
|
|
2936
|
-
session_id: "sess-
|
|
3493
|
+
session_id: "sess-stop-deep-interview-question-next-round",
|
|
2937
3494
|
},
|
|
2938
3495
|
{ cwd },
|
|
2939
3496
|
);
|
|
2940
3497
|
|
|
2941
3498
|
assert.equal(result.omxEventName, "stop");
|
|
2942
|
-
assert.
|
|
3499
|
+
assert.deepEqual(result.outputJson, {
|
|
3500
|
+
decision: "block",
|
|
3501
|
+
reason:
|
|
3502
|
+
"Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
3503
|
+
stopReason: "deep_interview_question_required",
|
|
3504
|
+
systemMessage:
|
|
3505
|
+
"OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
3506
|
+
});
|
|
2943
3507
|
} finally {
|
|
2944
3508
|
await rm(cwd, { recursive: true, force: true });
|
|
2945
3509
|
}
|
|
2946
3510
|
});
|
|
2947
3511
|
|
|
2948
|
-
it("
|
|
2949
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3512
|
+
it("ignores root skill-active fallback from a different thread when evaluating Stop", async () => {
|
|
3513
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-thread-"));
|
|
2950
3514
|
try {
|
|
2951
3515
|
const stateDir = join(cwd, ".omx", "state");
|
|
2952
3516
|
await mkdir(stateDir, { recursive: true });
|
|
2953
|
-
await writeJson(join(stateDir, "
|
|
2954
|
-
session_id: "sess-elsewhere",
|
|
2955
|
-
cwd: join(cwd, "..", "different-worktree"),
|
|
2956
|
-
});
|
|
2957
|
-
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3517
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
2958
3518
|
active: true,
|
|
2959
|
-
|
|
3519
|
+
skill: "deep-interview",
|
|
3520
|
+
phase: "planning",
|
|
3521
|
+
session_id: "",
|
|
3522
|
+
thread_id: "other-thread",
|
|
2960
3523
|
});
|
|
2961
3524
|
|
|
2962
3525
|
const result = await dispatchCodexNativeHook(
|
|
2963
3526
|
{
|
|
2964
3527
|
hook_event_name: "Stop",
|
|
2965
3528
|
cwd,
|
|
2966
|
-
session_id: "sess-
|
|
3529
|
+
session_id: "sess-stop-main",
|
|
3530
|
+
thread_id: "main-thread",
|
|
2967
3531
|
},
|
|
2968
3532
|
{ cwd },
|
|
2969
3533
|
);
|
|
2970
3534
|
|
|
2971
|
-
assert.equal(result.omxEventName, "stop");
|
|
2972
3535
|
assert.equal(result.outputJson, null);
|
|
2973
3536
|
} finally {
|
|
2974
3537
|
await rm(cwd, { recursive: true, force: true });
|
|
2975
3538
|
}
|
|
2976
3539
|
});
|
|
2977
3540
|
|
|
2978
|
-
it("
|
|
2979
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2980
|
-
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3541
|
+
it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
|
|
3542
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
|
|
2981
3543
|
try {
|
|
2982
3544
|
const stateDir = join(cwd, ".omx", "state");
|
|
2983
3545
|
await mkdir(stateDir, { recursive: true });
|
|
@@ -2989,55 +3551,45 @@ esac
|
|
|
2989
3551
|
}),
|
|
2990
3552
|
);
|
|
2991
3553
|
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3554
|
+
const result = await dispatchCodexNativeHook(
|
|
3555
|
+
{
|
|
3556
|
+
hook_event_name: "Stop",
|
|
3557
|
+
cwd,
|
|
3558
|
+
},
|
|
3559
|
+
{ cwd },
|
|
3560
|
+
);
|
|
3561
|
+
|
|
3562
|
+
assert.equal(result.omxEventName, "stop");
|
|
3563
|
+
assert.deepEqual(result.outputJson, {
|
|
2999
3564
|
decision: "block",
|
|
3000
3565
|
reason:
|
|
3001
3566
|
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3002
3567
|
stopReason: "ralph_executing",
|
|
3003
3568
|
systemMessage:
|
|
3004
3569
|
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3005
|
-
};
|
|
3006
|
-
|
|
3007
|
-
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
3008
|
-
const replay = await dispatchCodexNativeHook(
|
|
3009
|
-
{
|
|
3010
|
-
...payload,
|
|
3011
|
-
stop_hook_active: true,
|
|
3012
|
-
},
|
|
3013
|
-
{ cwd },
|
|
3014
|
-
);
|
|
3015
|
-
|
|
3016
|
-
assert.equal(first.omxEventName, "stop");
|
|
3017
|
-
assert.deepEqual(first.outputJson, expected);
|
|
3018
|
-
assert.equal(replay.omxEventName, "stop");
|
|
3019
|
-
assert.deepEqual(replay.outputJson, expected);
|
|
3570
|
+
});
|
|
3020
3571
|
} finally {
|
|
3021
|
-
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3022
|
-
else delete process.env.OMX_SESSION_ID;
|
|
3023
3572
|
await rm(cwd, { recursive: true, force: true });
|
|
3024
3573
|
}
|
|
3025
3574
|
});
|
|
3026
3575
|
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
3576
|
+
it("blocks Stop from session-scoped Ralph state when session.json points to another session", async () => {
|
|
3577
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-session-mismatch-"));
|
|
3030
3578
|
try {
|
|
3031
3579
|
const stateDir = join(cwd, ".omx", "state");
|
|
3032
|
-
await mkdir(stateDir, { recursive: true });
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3580
|
+
await mkdir(join(stateDir, "sessions", "sess-live-ralph"), { recursive: true });
|
|
3581
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-ralph" });
|
|
3582
|
+
await writeJson(join(stateDir, "sessions", "sess-live-ralph", "ralph-state.json"), {
|
|
3583
|
+
active: true,
|
|
3584
|
+
current_phase: "executing",
|
|
3585
|
+
session_id: "sess-live-ralph",
|
|
3586
|
+
});
|
|
3587
|
+
|
|
3588
|
+
const result = await dispatchCodexNativeHook(
|
|
3036
3589
|
{
|
|
3037
3590
|
hook_event_name: "Stop",
|
|
3038
3591
|
cwd,
|
|
3039
|
-
session_id: "sess-
|
|
3040
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3592
|
+
session_id: "sess-live-ralph",
|
|
3041
3593
|
},
|
|
3042
3594
|
{ cwd },
|
|
3043
3595
|
);
|
|
@@ -3045,258 +3597,201 @@ esac
|
|
|
3045
3597
|
assert.equal(result.omxEventName, "stop");
|
|
3046
3598
|
assert.deepEqual(result.outputJson, {
|
|
3047
3599
|
decision: "block",
|
|
3048
|
-
reason:
|
|
3049
|
-
|
|
3600
|
+
reason:
|
|
3601
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3602
|
+
stopReason: "ralph_executing",
|
|
3050
3603
|
systemMessage:
|
|
3051
|
-
"OMX
|
|
3604
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3052
3605
|
});
|
|
3053
3606
|
} finally {
|
|
3054
3607
|
await rm(cwd, { recursive: true, force: true });
|
|
3055
3608
|
}
|
|
3056
3609
|
});
|
|
3057
3610
|
|
|
3058
|
-
it("
|
|
3059
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3611
|
+
it("does not block Stop from stale session-scoped Ralph state that belongs to another session", async () => {
|
|
3612
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralph-"));
|
|
3060
3613
|
try {
|
|
3061
3614
|
const stateDir = join(cwd, ".omx", "state");
|
|
3062
|
-
await mkdir(stateDir, { recursive: true });
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
await
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
thread_id: "thread-stop-auto",
|
|
3071
|
-
turn_id: "turn-stop-auto-1",
|
|
3072
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3073
|
-
},
|
|
3074
|
-
{ cwd },
|
|
3075
|
-
);
|
|
3615
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3616
|
+
await mkdir(join(stateDir, "sessions", "sess-stale"), { recursive: true });
|
|
3617
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
3618
|
+
await writeJson(join(stateDir, "sessions", "sess-stale", "ralph-state.json"), {
|
|
3619
|
+
active: true,
|
|
3620
|
+
current_phase: "starting",
|
|
3621
|
+
session_id: "sess-stale",
|
|
3622
|
+
});
|
|
3076
3623
|
|
|
3077
3624
|
const result = await dispatchCodexNativeHook(
|
|
3078
3625
|
{
|
|
3079
3626
|
hook_event_name: "Stop",
|
|
3080
3627
|
cwd,
|
|
3081
|
-
session_id: "sess-
|
|
3082
|
-
thread_id: "thread-stop-auto",
|
|
3083
|
-
turn_id: "turn-stop-auto-1",
|
|
3084
|
-
stop_hook_active: true,
|
|
3085
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3628
|
+
session_id: "sess-current",
|
|
3086
3629
|
},
|
|
3087
3630
|
{ cwd },
|
|
3088
3631
|
);
|
|
3089
3632
|
|
|
3090
3633
|
assert.equal(result.omxEventName, "stop");
|
|
3091
|
-
assert.
|
|
3092
|
-
decision: "block",
|
|
3093
|
-
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3094
|
-
stopReason: "auto_nudge",
|
|
3095
|
-
systemMessage:
|
|
3096
|
-
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3097
|
-
});
|
|
3634
|
+
assert.equal(result.outputJson, null);
|
|
3098
3635
|
} finally {
|
|
3099
3636
|
await rm(cwd, { recursive: true, force: true });
|
|
3100
3637
|
}
|
|
3101
3638
|
});
|
|
3102
3639
|
|
|
3103
|
-
it("
|
|
3104
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3640
|
+
it("does not block Stop from stale current-session Ralph state when session.json points to a dead owner", async () => {
|
|
3641
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-current-session-ralph-"));
|
|
3105
3642
|
try {
|
|
3106
3643
|
const stateDir = join(cwd, ".omx", "state");
|
|
3107
|
-
await mkdir(stateDir, { recursive: true });
|
|
3108
|
-
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
3644
|
+
await mkdir(join(stateDir, "sessions", "sess-dead"), { recursive: true });
|
|
3109
3645
|
await writeJson(join(stateDir, "session.json"), {
|
|
3110
|
-
session_id: "
|
|
3111
|
-
|
|
3646
|
+
session_id: "sess-dead",
|
|
3647
|
+
cwd,
|
|
3648
|
+
pid: Number.MAX_SAFE_INTEGER,
|
|
3649
|
+
started_at: "2026-01-01T00:00:00.000Z",
|
|
3112
3650
|
});
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3651
|
+
await writeJson(join(stateDir, "sessions", "sess-dead", "ralph-state.json"), {
|
|
3652
|
+
active: true,
|
|
3653
|
+
current_phase: "verifying",
|
|
3654
|
+
session_id: "sess-dead",
|
|
3655
|
+
});
|
|
3656
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
3657
|
+
active: true,
|
|
3658
|
+
skill: "team",
|
|
3659
|
+
phase: "team-exec",
|
|
3660
|
+
active_skills: [{ skill: "team", phase: "team-exec", active: true, session_id: "sess-dead" }],
|
|
3661
|
+
});
|
|
3662
|
+
await writeJson(join(stateDir, "native-stop-state.json"), {
|
|
3663
|
+
sessions: {
|
|
3664
|
+
"sess-dead": {
|
|
3665
|
+
last_signature: "ralph-stop|sess-dead|thread-1|no-message|verifying",
|
|
3666
|
+
updated_at: "2026-04-20T21:00:00.000Z",
|
|
3667
|
+
},
|
|
3122
3668
|
},
|
|
3123
|
-
|
|
3124
|
-
);
|
|
3669
|
+
});
|
|
3125
3670
|
|
|
3126
3671
|
const result = await dispatchCodexNativeHook(
|
|
3127
3672
|
{
|
|
3128
3673
|
hook_event_name: "Stop",
|
|
3129
3674
|
cwd,
|
|
3130
|
-
session_id: "
|
|
3131
|
-
thread_id: "thread-
|
|
3132
|
-
turn_id: "turn-stop-auto-drift-1",
|
|
3675
|
+
session_id: "sess-dead",
|
|
3676
|
+
thread_id: "thread-1",
|
|
3133
3677
|
stop_hook_active: true,
|
|
3134
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3135
3678
|
},
|
|
3136
3679
|
{ cwd },
|
|
3137
3680
|
);
|
|
3138
3681
|
|
|
3139
3682
|
assert.equal(result.omxEventName, "stop");
|
|
3140
|
-
assert.
|
|
3141
|
-
decision: "block",
|
|
3142
|
-
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3143
|
-
stopReason: "auto_nudge",
|
|
3144
|
-
systemMessage:
|
|
3145
|
-
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3146
|
-
});
|
|
3147
|
-
|
|
3148
|
-
const persisted = JSON.parse(
|
|
3149
|
-
await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
|
|
3150
|
-
) as { sessions?: Record<string, unknown> };
|
|
3151
|
-
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
3683
|
+
assert.equal(result.outputJson, null);
|
|
3152
3684
|
} finally {
|
|
3153
3685
|
await rm(cwd, { recursive: true, force: true });
|
|
3154
3686
|
}
|
|
3155
3687
|
});
|
|
3156
3688
|
|
|
3157
|
-
it("
|
|
3158
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3689
|
+
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
3690
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
3159
3691
|
try {
|
|
3160
3692
|
const stateDir = join(cwd, ".omx", "state");
|
|
3161
|
-
await mkdir(stateDir, { recursive: true });
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
cwd,
|
|
3168
|
-
session_id: "sess-stop-auto-refire",
|
|
3169
|
-
thread_id: "thread-stop-auto-refire",
|
|
3170
|
-
turn_id: "turn-stop-auto-refire-1",
|
|
3171
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3172
|
-
},
|
|
3173
|
-
{ cwd },
|
|
3174
|
-
);
|
|
3693
|
+
await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
|
|
3694
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "ralph-state.json"), {
|
|
3695
|
+
active: true,
|
|
3696
|
+
current_phase: "starting",
|
|
3697
|
+
session_id: "sess-other",
|
|
3698
|
+
});
|
|
3175
3699
|
|
|
3176
3700
|
const result = await dispatchCodexNativeHook(
|
|
3177
3701
|
{
|
|
3178
3702
|
hook_event_name: "Stop",
|
|
3179
3703
|
cwd,
|
|
3180
|
-
session_id: "sess-
|
|
3181
|
-
thread_id: "thread-stop-auto-refire",
|
|
3182
|
-
turn_id: "turn-stop-auto-refire-2",
|
|
3183
|
-
stop_hook_active: true,
|
|
3184
|
-
last_assistant_message: "Continue with the cleanup from here.",
|
|
3704
|
+
session_id: "sess-current",
|
|
3185
3705
|
},
|
|
3186
3706
|
{ cwd },
|
|
3187
3707
|
);
|
|
3188
3708
|
|
|
3189
3709
|
assert.equal(result.omxEventName, "stop");
|
|
3190
|
-
assert.
|
|
3191
|
-
decision: "block",
|
|
3192
|
-
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3193
|
-
stopReason: "auto_nudge",
|
|
3194
|
-
systemMessage:
|
|
3195
|
-
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3196
|
-
});
|
|
3710
|
+
assert.equal(result.outputJson, null);
|
|
3197
3711
|
} finally {
|
|
3198
3712
|
await rm(cwd, { recursive: true, force: true });
|
|
3199
3713
|
}
|
|
3200
3714
|
});
|
|
3201
3715
|
|
|
3202
|
-
it("
|
|
3203
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3716
|
+
it("does not block Stop from root Ralph fallback when the current session has no scoped Ralph state", async () => {
|
|
3717
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-fallback-ralph-"));
|
|
3204
3718
|
try {
|
|
3205
|
-
|
|
3206
|
-
|
|
3719
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3720
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3721
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current", cwd });
|
|
3722
|
+
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3723
|
+
active: true,
|
|
3724
|
+
current_phase: "executing",
|
|
3725
|
+
});
|
|
3207
3726
|
|
|
3208
3727
|
const result = await dispatchCodexNativeHook(
|
|
3209
3728
|
{
|
|
3210
3729
|
hook_event_name: "Stop",
|
|
3211
3730
|
cwd,
|
|
3212
|
-
session_id: "sess-
|
|
3213
|
-
last_assistant_message: "Would you like me to continue with the cleanup?",
|
|
3731
|
+
session_id: "sess-current",
|
|
3214
3732
|
},
|
|
3215
3733
|
{ cwd },
|
|
3216
3734
|
);
|
|
3217
3735
|
|
|
3218
3736
|
assert.equal(result.omxEventName, "stop");
|
|
3219
|
-
assert.
|
|
3220
|
-
decision: "block",
|
|
3221
|
-
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3222
|
-
stopReason: "auto_nudge",
|
|
3223
|
-
systemMessage:
|
|
3224
|
-
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3225
|
-
});
|
|
3737
|
+
assert.equal(result.outputJson, null);
|
|
3226
3738
|
} finally {
|
|
3227
3739
|
await rm(cwd, { recursive: true, force: true });
|
|
3228
3740
|
}
|
|
3229
3741
|
});
|
|
3230
3742
|
|
|
3231
|
-
it("
|
|
3232
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3743
|
+
it("does not block Stop when the current session Ralph state is cancelled even if stale root fallback remains", async () => {
|
|
3744
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-cancelled-session-ralph-"));
|
|
3233
3745
|
try {
|
|
3234
|
-
|
|
3235
|
-
|
|
3746
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3747
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3748
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current", cwd });
|
|
3749
|
+
await writeJson(join(stateDir, "sessions", "sess-current", "ralph-state.json"), {
|
|
3750
|
+
active: false,
|
|
3751
|
+
current_phase: "cancelled",
|
|
3752
|
+
completed_at: "2026-04-10T23:30:38.000Z",
|
|
3753
|
+
session_id: "sess-current",
|
|
3754
|
+
});
|
|
3755
|
+
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3756
|
+
active: true,
|
|
3757
|
+
current_phase: "starting",
|
|
3758
|
+
});
|
|
3236
3759
|
|
|
3237
3760
|
const result = await dispatchCodexNativeHook(
|
|
3238
3761
|
{
|
|
3239
3762
|
hook_event_name: "Stop",
|
|
3240
3763
|
cwd,
|
|
3241
|
-
session_id: "sess-
|
|
3242
|
-
last_assistant_message: "If you want, I can continue with the cleanup from here.",
|
|
3764
|
+
session_id: "sess-current",
|
|
3243
3765
|
},
|
|
3244
3766
|
{ cwd },
|
|
3245
3767
|
);
|
|
3246
3768
|
|
|
3247
3769
|
assert.equal(result.omxEventName, "stop");
|
|
3248
|
-
assert.
|
|
3249
|
-
decision: "block",
|
|
3250
|
-
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3251
|
-
stopReason: "auto_nudge",
|
|
3252
|
-
systemMessage:
|
|
3253
|
-
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3254
|
-
});
|
|
3770
|
+
assert.equal(result.outputJson, null);
|
|
3255
3771
|
} finally {
|
|
3256
3772
|
await rm(cwd, { recursive: true, force: true });
|
|
3257
3773
|
}
|
|
3258
3774
|
});
|
|
3259
3775
|
|
|
3260
|
-
it("does not
|
|
3261
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3776
|
+
it("does not block Stop from root Ralph fallback when an explicit session_id is present and session.json points to another worktree", async () => {
|
|
3777
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-fallback-cwd-mismatch-"));
|
|
3262
3778
|
try {
|
|
3263
3779
|
const stateDir = join(cwd, ".omx", "state");
|
|
3264
|
-
await mkdir(
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
version: 1,
|
|
3269
|
-
active: true,
|
|
3270
|
-
skill: "deep-interview",
|
|
3271
|
-
phase: "planning",
|
|
3272
|
-
session_id: "sess-stop-auto-question",
|
|
3273
|
-
thread_id: "thread-stop-auto-question",
|
|
3274
|
-
input_lock: {
|
|
3275
|
-
active: true,
|
|
3276
|
-
scope: "deep-interview-auto-approval",
|
|
3277
|
-
blocked_inputs: ["yes", "proceed"],
|
|
3278
|
-
message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
|
|
3279
|
-
},
|
|
3780
|
+
await mkdir(stateDir, { recursive: true });
|
|
3781
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3782
|
+
session_id: "sess-elsewhere",
|
|
3783
|
+
cwd: join(cwd, "..", "different-worktree"),
|
|
3280
3784
|
});
|
|
3281
|
-
await writeJson(join(stateDir, "
|
|
3785
|
+
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3282
3786
|
active: true,
|
|
3283
|
-
|
|
3284
|
-
current_phase: "intent-first",
|
|
3787
|
+
current_phase: "executing",
|
|
3285
3788
|
});
|
|
3286
3789
|
|
|
3287
3790
|
const result = await dispatchCodexNativeHook(
|
|
3288
3791
|
{
|
|
3289
3792
|
hook_event_name: "Stop",
|
|
3290
3793
|
cwd,
|
|
3291
|
-
session_id: "sess-
|
|
3292
|
-
thread_id: "thread-stop-auto-question",
|
|
3293
|
-
turn_id: "turn-stop-auto-question-1",
|
|
3294
|
-
last_assistant_message: [
|
|
3295
|
-
"Round 2 | Target: Decision boundary | Ambiguity: 24%",
|
|
3296
|
-
"",
|
|
3297
|
-
"If an existing project spider still declares session_mode = \"owned\", should ZenX fail loudly so the stale attribute is removed, or should it ignore the attribute and initialize the session pool anyway?",
|
|
3298
|
-
"Keep going once I have your answer.",
|
|
3299
|
-
].join("\n"),
|
|
3794
|
+
session_id: "sess-current",
|
|
3300
3795
|
},
|
|
3301
3796
|
{ cwd },
|
|
3302
3797
|
);
|
|
@@ -3308,124 +3803,179 @@ esac
|
|
|
3308
3803
|
}
|
|
3309
3804
|
});
|
|
3310
3805
|
|
|
3311
|
-
it("
|
|
3312
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3806
|
+
it("keeps blocking Ralph Stop replays until the active task advances", async () => {
|
|
3807
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-replay-"));
|
|
3808
|
+
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3313
3809
|
try {
|
|
3314
3810
|
const stateDir = join(cwd, ".omx", "state");
|
|
3315
|
-
await mkdir(
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3811
|
+
await mkdir(stateDir, { recursive: true });
|
|
3812
|
+
await writeFile(
|
|
3813
|
+
join(stateDir, "ralph-state.json"),
|
|
3814
|
+
JSON.stringify({
|
|
3815
|
+
active: true,
|
|
3816
|
+
current_phase: "executing",
|
|
3817
|
+
}),
|
|
3818
|
+
);
|
|
3323
3819
|
|
|
3324
|
-
|
|
3820
|
+
process.env.OMX_SESSION_ID = "sess-stop-ralph-replay";
|
|
3821
|
+
const payload = {
|
|
3822
|
+
hook_event_name: "Stop",
|
|
3823
|
+
cwd,
|
|
3824
|
+
last_assistant_message: "Next active targets:\n\n1. scheduler integration\n\nI am continuing.",
|
|
3825
|
+
};
|
|
3826
|
+
const expected = {
|
|
3827
|
+
decision: "block",
|
|
3828
|
+
reason:
|
|
3829
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3830
|
+
stopReason: "ralph_executing",
|
|
3831
|
+
systemMessage:
|
|
3832
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3833
|
+
};
|
|
3834
|
+
|
|
3835
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
3836
|
+
const replay = await dispatchCodexNativeHook(
|
|
3325
3837
|
{
|
|
3326
|
-
|
|
3327
|
-
cwd,
|
|
3328
|
-
session_id: "sess-stop-auto-interview",
|
|
3329
|
-
thread_id: "thread-stop-auto-interview",
|
|
3330
|
-
turn_id: "turn-stop-auto-interview-2",
|
|
3838
|
+
...payload,
|
|
3331
3839
|
stop_hook_active: true,
|
|
3332
|
-
last_assistant_message: "If you want, I can keep going from here.",
|
|
3333
3840
|
},
|
|
3334
3841
|
{ cwd },
|
|
3335
3842
|
);
|
|
3336
3843
|
|
|
3337
|
-
assert.equal(
|
|
3338
|
-
assert.
|
|
3844
|
+
assert.equal(first.omxEventName, "stop");
|
|
3845
|
+
assert.deepEqual(first.outputJson, expected);
|
|
3846
|
+
assert.equal(replay.omxEventName, "stop");
|
|
3847
|
+
assert.deepEqual(replay.outputJson, expected);
|
|
3339
3848
|
} finally {
|
|
3849
|
+
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3850
|
+
else delete process.env.OMX_SESSION_ID;
|
|
3340
3851
|
await rm(cwd, { recursive: true, force: true });
|
|
3341
3852
|
}
|
|
3342
3853
|
});
|
|
3343
3854
|
|
|
3344
|
-
it("
|
|
3345
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3855
|
+
it("lets dispatcher dedupe identical native stop hook replays after Stop payload normalization", async () => {
|
|
3856
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-hook-dedupe-"));
|
|
3857
|
+
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3346
3858
|
try {
|
|
3347
3859
|
const stateDir = join(cwd, ".omx", "state");
|
|
3348
|
-
await mkdir(stateDir, { recursive: true });
|
|
3349
|
-
|
|
3350
|
-
await
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3860
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ralph-hook-dedupe"), { recursive: true });
|
|
3861
|
+
await writeHookCounterPlugin(cwd);
|
|
3862
|
+
await writeFile(
|
|
3863
|
+
join(stateDir, "sessions", "sess-stop-ralph-hook-dedupe", "ralph-state.json"),
|
|
3864
|
+
JSON.stringify({
|
|
3865
|
+
active: true,
|
|
3866
|
+
current_phase: "executing",
|
|
3867
|
+
session_id: "sess-stop-ralph-hook-dedupe",
|
|
3868
|
+
}),
|
|
3869
|
+
);
|
|
3355
3870
|
|
|
3356
|
-
|
|
3871
|
+
process.env.OMX_SESSION_ID = "sess-stop-ralph-hook-dedupe";
|
|
3872
|
+
const payload = {
|
|
3873
|
+
hook_event_name: "Stop",
|
|
3874
|
+
cwd,
|
|
3875
|
+
session_id: "sess-stop-ralph-hook-dedupe",
|
|
3876
|
+
thread_id: "thread-stop-ralph-hook-dedupe",
|
|
3877
|
+
turn_id: "turn-stop-ralph-hook-dedupe-1",
|
|
3878
|
+
last_assistant_message: "Next active targets:\n\n1. scheduler integration\n\nI am continuing.",
|
|
3879
|
+
};
|
|
3880
|
+
|
|
3881
|
+
await dispatchCodexNativeHook(payload, { cwd });
|
|
3882
|
+
await dispatchCodexNativeHook(
|
|
3357
3883
|
{
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
turn_id: "turn-stop-auto-mode-1",
|
|
3361
|
-
last_assistant_message: "Would you like me to continue with the next step?",
|
|
3884
|
+
...payload,
|
|
3885
|
+
stop_hook_active: true,
|
|
3362
3886
|
},
|
|
3363
3887
|
{ cwd },
|
|
3364
3888
|
);
|
|
3365
3889
|
|
|
3366
|
-
|
|
3367
|
-
|
|
3890
|
+
const marker = JSON.parse(
|
|
3891
|
+
await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"),
|
|
3892
|
+
) as { count: number };
|
|
3893
|
+
assert.equal(marker.count, 1);
|
|
3368
3894
|
} finally {
|
|
3895
|
+
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3896
|
+
else delete process.env.OMX_SESSION_ID;
|
|
3369
3897
|
await rm(cwd, { recursive: true, force: true });
|
|
3370
3898
|
}
|
|
3371
3899
|
});
|
|
3372
3900
|
|
|
3373
|
-
it("
|
|
3374
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3901
|
+
it("preserves per-turn native stop hook delivery even when stop_hook_active remains true", async () => {
|
|
3902
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-hook-refire-"));
|
|
3903
|
+
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3375
3904
|
try {
|
|
3376
3905
|
const stateDir = join(cwd, ".omx", "state");
|
|
3377
|
-
await mkdir(stateDir, { recursive: true });
|
|
3378
|
-
|
|
3379
|
-
await
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3906
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ralph-hook-refire"), { recursive: true });
|
|
3907
|
+
await writeHookCounterPlugin(cwd);
|
|
3908
|
+
await writeFile(
|
|
3909
|
+
join(stateDir, "sessions", "sess-stop-ralph-hook-refire", "ralph-state.json"),
|
|
3910
|
+
JSON.stringify({
|
|
3911
|
+
active: true,
|
|
3912
|
+
current_phase: "executing",
|
|
3913
|
+
session_id: "sess-stop-ralph-hook-refire",
|
|
3914
|
+
}),
|
|
3915
|
+
);
|
|
3384
3916
|
|
|
3385
|
-
|
|
3917
|
+
process.env.OMX_SESSION_ID = "sess-stop-ralph-hook-refire";
|
|
3918
|
+
const payload = {
|
|
3919
|
+
hook_event_name: "Stop",
|
|
3920
|
+
cwd,
|
|
3921
|
+
session_id: "sess-stop-ralph-hook-refire",
|
|
3922
|
+
thread_id: "thread-stop-ralph-hook-refire",
|
|
3923
|
+
turn_id: "turn-stop-ralph-hook-refire-1",
|
|
3924
|
+
last_assistant_message: "Continuing current task.",
|
|
3925
|
+
};
|
|
3926
|
+
|
|
3927
|
+
await dispatchCodexNativeHook(payload, { cwd });
|
|
3928
|
+
await dispatchCodexNativeHook(
|
|
3386
3929
|
{
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
thread_id: "thread-stop-auto-stale-root-mode",
|
|
3391
|
-
turn_id: "turn-stop-auto-stale-root-mode-1",
|
|
3392
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3930
|
+
...payload,
|
|
3931
|
+
turn_id: "turn-stop-ralph-hook-refire-2",
|
|
3932
|
+
stop_hook_active: true,
|
|
3393
3933
|
},
|
|
3394
3934
|
{ cwd },
|
|
3395
3935
|
);
|
|
3396
3936
|
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3937
|
+
await writeFile(
|
|
3938
|
+
join(stateDir, "sessions", "sess-stop-ralph-hook-refire", "ralph-state.json"),
|
|
3939
|
+
JSON.stringify({
|
|
3940
|
+
active: true,
|
|
3941
|
+
current_phase: "executing",
|
|
3942
|
+
session_id: "sess-stop-ralph-hook-refire",
|
|
3943
|
+
}),
|
|
3944
|
+
);
|
|
3945
|
+
|
|
3946
|
+
await dispatchCodexNativeHook(
|
|
3947
|
+
{
|
|
3948
|
+
...payload,
|
|
3949
|
+
turn_id: "turn-stop-ralph-hook-refire-3",
|
|
3950
|
+
stop_hook_active: true,
|
|
3951
|
+
},
|
|
3952
|
+
{ cwd },
|
|
3953
|
+
);
|
|
3954
|
+
|
|
3955
|
+
const marker = JSON.parse(
|
|
3956
|
+
await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"),
|
|
3957
|
+
) as { count: number };
|
|
3958
|
+
assert.equal(marker.count, 3);
|
|
3405
3959
|
} finally {
|
|
3960
|
+
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3961
|
+
else delete process.env.OMX_SESSION_ID;
|
|
3406
3962
|
await rm(cwd, { recursive: true, force: true });
|
|
3407
3963
|
}
|
|
3408
3964
|
});
|
|
3409
3965
|
|
|
3410
|
-
|
|
3411
|
-
|
|
3966
|
+
|
|
3967
|
+
it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
|
|
3968
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
3412
3969
|
try {
|
|
3413
3970
|
const stateDir = join(cwd, ".omx", "state");
|
|
3414
3971
|
await mkdir(stateDir, { recursive: true });
|
|
3415
|
-
process.env.OMX_SESSION_ID = "sess-stop-auto
|
|
3416
|
-
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
3417
|
-
active: true,
|
|
3418
|
-
skill: "deep-interview",
|
|
3419
|
-
phase: "planning",
|
|
3420
|
-
});
|
|
3972
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto";
|
|
3421
3973
|
|
|
3422
3974
|
const result = await dispatchCodexNativeHook(
|
|
3423
3975
|
{
|
|
3424
3976
|
hook_event_name: "Stop",
|
|
3425
3977
|
cwd,
|
|
3426
|
-
session_id: "sess-stop-auto
|
|
3427
|
-
thread_id: "thread-stop-auto-stale-root-skill",
|
|
3428
|
-
turn_id: "turn-stop-auto-stale-root-skill-1",
|
|
3978
|
+
session_id: "sess-stop-auto",
|
|
3429
3979
|
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3430
3980
|
},
|
|
3431
3981
|
{ cwd },
|
|
@@ -3444,31 +3994,33 @@ esac
|
|
|
3444
3994
|
}
|
|
3445
3995
|
});
|
|
3446
3996
|
|
|
3447
|
-
it("
|
|
3448
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-
|
|
3997
|
+
it("re-blocks duplicate native auto-nudge replays for the same Stop reply", async () => {
|
|
3998
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-once-"));
|
|
3449
3999
|
try {
|
|
3450
4000
|
const stateDir = join(cwd, ".omx", "state");
|
|
3451
4001
|
await mkdir(stateDir, { recursive: true });
|
|
3452
|
-
process.env.OMX_SESSION_ID = "sess-stop-auto-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
4002
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-once";
|
|
4003
|
+
|
|
4004
|
+
await dispatchCodexNativeHook(
|
|
4005
|
+
{
|
|
4006
|
+
hook_event_name: "Stop",
|
|
4007
|
+
cwd,
|
|
4008
|
+
session_id: "sess-stop-auto-once",
|
|
4009
|
+
thread_id: "thread-stop-auto",
|
|
4010
|
+
turn_id: "turn-stop-auto-1",
|
|
4011
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3462
4012
|
},
|
|
3463
|
-
|
|
4013
|
+
{ cwd },
|
|
4014
|
+
);
|
|
3464
4015
|
|
|
3465
4016
|
const result = await dispatchCodexNativeHook(
|
|
3466
4017
|
{
|
|
3467
4018
|
hook_event_name: "Stop",
|
|
3468
4019
|
cwd,
|
|
3469
|
-
session_id: "sess-stop-auto-
|
|
3470
|
-
thread_id: "thread-stop-auto
|
|
3471
|
-
turn_id: "turn-stop-auto-
|
|
4020
|
+
session_id: "sess-stop-auto-once",
|
|
4021
|
+
thread_id: "thread-stop-auto",
|
|
4022
|
+
turn_id: "turn-stop-auto-1",
|
|
4023
|
+
stop_hook_active: true,
|
|
3472
4024
|
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3473
4025
|
},
|
|
3474
4026
|
{ cwd },
|
|
@@ -3487,31 +4039,37 @@ esac
|
|
|
3487
4039
|
}
|
|
3488
4040
|
});
|
|
3489
4041
|
|
|
3490
|
-
it("
|
|
3491
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-
|
|
4042
|
+
it("re-blocks duplicate native auto-nudge replays across native/canonical session-id drift", async () => {
|
|
4043
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-session-drift-"));
|
|
3492
4044
|
try {
|
|
3493
4045
|
const stateDir = join(cwd, ".omx", "state");
|
|
3494
|
-
await mkdir(
|
|
3495
|
-
process.env.OMX_SESSION_ID = "
|
|
3496
|
-
await writeJson(join(stateDir, "session.json"), {
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
mode: "deep-interview",
|
|
3500
|
-
current_phase: "completed",
|
|
3501
|
-
});
|
|
3502
|
-
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
3503
|
-
active: true,
|
|
3504
|
-
mode: "deep-interview",
|
|
3505
|
-
current_phase: "intent-first",
|
|
4046
|
+
await mkdir(stateDir, { recursive: true });
|
|
4047
|
+
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
4048
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
4049
|
+
session_id: "omx-canonical",
|
|
4050
|
+
native_session_id: "codex-native",
|
|
3506
4051
|
});
|
|
3507
4052
|
|
|
4053
|
+
await dispatchCodexNativeHook(
|
|
4054
|
+
{
|
|
4055
|
+
hook_event_name: "Stop",
|
|
4056
|
+
cwd,
|
|
4057
|
+
session_id: "codex-native",
|
|
4058
|
+
thread_id: "thread-stop-auto-drift",
|
|
4059
|
+
turn_id: "turn-stop-auto-drift-1",
|
|
4060
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
4061
|
+
},
|
|
4062
|
+
{ cwd },
|
|
4063
|
+
);
|
|
4064
|
+
|
|
3508
4065
|
const result = await dispatchCodexNativeHook(
|
|
3509
4066
|
{
|
|
3510
4067
|
hook_event_name: "Stop",
|
|
3511
4068
|
cwd,
|
|
3512
|
-
session_id: "
|
|
3513
|
-
thread_id: "thread-stop-auto-
|
|
3514
|
-
turn_id: "turn-stop-auto-
|
|
4069
|
+
session_id: "omx-canonical",
|
|
4070
|
+
thread_id: "thread-stop-auto-drift",
|
|
4071
|
+
turn_id: "turn-stop-auto-drift-1",
|
|
4072
|
+
stop_hook_active: true,
|
|
3515
4073
|
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3516
4074
|
},
|
|
3517
4075
|
{ cwd },
|
|
@@ -3525,76 +4083,94 @@ esac
|
|
|
3525
4083
|
systemMessage:
|
|
3526
4084
|
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3527
4085
|
});
|
|
4086
|
+
|
|
4087
|
+
const persisted = JSON.parse(
|
|
4088
|
+
await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
|
|
4089
|
+
) as { sessions?: Record<string, unknown> };
|
|
4090
|
+
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
3528
4091
|
} finally {
|
|
3529
4092
|
await rm(cwd, { recursive: true, force: true });
|
|
3530
4093
|
}
|
|
3531
4094
|
});
|
|
3532
4095
|
|
|
3533
|
-
it("
|
|
3534
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
4096
|
+
it("dedupes native stop hook replay across owner launch SessionStart reconciliation drift", async () => {
|
|
4097
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-dispatch-session-drift-"));
|
|
3535
4098
|
try {
|
|
4099
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4100
|
+
await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
|
|
4101
|
+
await writeHookCounterPlugin(cwd);
|
|
4102
|
+
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
4103
|
+
await writeSessionStart(cwd, "omx-canonical");
|
|
4104
|
+
await writeJson(join(stateDir, "sessions", "omx-canonical", "ralph-state.json"), {
|
|
4105
|
+
active: true,
|
|
4106
|
+
current_phase: "executing",
|
|
4107
|
+
session_id: "omx-canonical",
|
|
4108
|
+
});
|
|
4109
|
+
|
|
3536
4110
|
await dispatchCodexNativeHook(
|
|
3537
4111
|
{
|
|
3538
4112
|
hook_event_name: "SessionStart",
|
|
3539
4113
|
cwd,
|
|
3540
|
-
session_id: "
|
|
4114
|
+
session_id: "codex-native-new",
|
|
3541
4115
|
},
|
|
4116
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
4117
|
+
);
|
|
4118
|
+
|
|
4119
|
+
await dispatchCodexNativeHook(
|
|
3542
4120
|
{
|
|
4121
|
+
hook_event_name: "Stop",
|
|
3543
4122
|
cwd,
|
|
3544
|
-
|
|
4123
|
+
session_id: "codex-native-new",
|
|
4124
|
+
thread_id: "thread-stop-hook-drift",
|
|
4125
|
+
turn_id: "turn-stop-hook-drift-1",
|
|
4126
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3545
4127
|
},
|
|
4128
|
+
{ cwd },
|
|
3546
4129
|
);
|
|
3547
4130
|
|
|
3548
|
-
|
|
4131
|
+
await dispatchCodexNativeHook(
|
|
3549
4132
|
{
|
|
3550
4133
|
hook_event_name: "Stop",
|
|
3551
4134
|
cwd,
|
|
3552
|
-
session_id: "
|
|
3553
|
-
thread_id: "
|
|
3554
|
-
turn_id: "
|
|
3555
|
-
|
|
4135
|
+
session_id: "omx-canonical",
|
|
4136
|
+
thread_id: "thread-stop-hook-drift",
|
|
4137
|
+
turn_id: "turn-stop-hook-drift-1",
|
|
4138
|
+
stop_hook_active: true,
|
|
4139
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3556
4140
|
},
|
|
3557
4141
|
{ cwd },
|
|
3558
4142
|
);
|
|
3559
4143
|
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
4144
|
+
const marker = JSON.parse(
|
|
4145
|
+
await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"),
|
|
4146
|
+
) as { count: number };
|
|
4147
|
+
assert.equal(marker.count, 1);
|
|
4148
|
+
|
|
4149
|
+
const sessionState = JSON.parse(
|
|
4150
|
+
await readFile(join(stateDir, "session.json"), "utf-8"),
|
|
4151
|
+
) as { session_id?: string; native_session_id?: string };
|
|
4152
|
+
assert.equal(sessionState.session_id, "omx-canonical");
|
|
4153
|
+
assert.equal(sessionState.native_session_id, "codex-native-new");
|
|
4154
|
+
} finally {
|
|
4155
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3570
4156
|
}
|
|
3571
4157
|
});
|
|
3572
4158
|
|
|
3573
|
-
it("re-fires
|
|
3574
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
4159
|
+
it("re-fires native auto-nudge for a later fresh Stop reply even when stop_hook_active is true", async () => {
|
|
4160
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-refire-"));
|
|
3575
4161
|
try {
|
|
3576
4162
|
const stateDir = join(cwd, ".omx", "state");
|
|
3577
4163
|
await mkdir(stateDir, { recursive: true });
|
|
3578
|
-
|
|
3579
|
-
active: true,
|
|
3580
|
-
current_phase: "team-exec",
|
|
3581
|
-
team_name: "review-team",
|
|
3582
|
-
});
|
|
3583
|
-
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
3584
|
-
current_phase: "team-verify",
|
|
3585
|
-
max_fix_attempts: 3,
|
|
3586
|
-
current_fix_attempt: 0,
|
|
3587
|
-
transitions: [],
|
|
3588
|
-
updated_at: new Date().toISOString(),
|
|
3589
|
-
});
|
|
4164
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-refire";
|
|
3590
4165
|
|
|
3591
4166
|
await dispatchCodexNativeHook(
|
|
3592
4167
|
{
|
|
3593
4168
|
hook_event_name: "Stop",
|
|
3594
4169
|
cwd,
|
|
3595
|
-
session_id: "sess-stop-
|
|
3596
|
-
thread_id: "thread-stop-
|
|
3597
|
-
turn_id: "turn-stop-
|
|
4170
|
+
session_id: "sess-stop-auto-refire",
|
|
4171
|
+
thread_id: "thread-stop-auto-refire",
|
|
4172
|
+
turn_id: "turn-stop-auto-refire-1",
|
|
4173
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3598
4174
|
},
|
|
3599
4175
|
{ cwd },
|
|
3600
4176
|
);
|
|
@@ -3603,10 +4179,11 @@ esac
|
|
|
3603
4179
|
{
|
|
3604
4180
|
hook_event_name: "Stop",
|
|
3605
4181
|
cwd,
|
|
3606
|
-
session_id: "sess-stop-
|
|
3607
|
-
thread_id: "thread-stop-
|
|
3608
|
-
turn_id: "turn-stop-
|
|
4182
|
+
session_id: "sess-stop-auto-refire",
|
|
4183
|
+
thread_id: "thread-stop-auto-refire",
|
|
4184
|
+
turn_id: "turn-stop-auto-refire-2",
|
|
3609
4185
|
stop_hook_active: true,
|
|
4186
|
+
last_assistant_message: "Continue with the cleanup from here.",
|
|
3610
4187
|
},
|
|
3611
4188
|
{ cwd },
|
|
3612
4189
|
);
|
|
@@ -3614,379 +4191,1483 @@ esac
|
|
|
3614
4191
|
assert.equal(result.omxEventName, "stop");
|
|
3615
4192
|
assert.deepEqual(result.outputJson, {
|
|
3616
4193
|
decision: "block",
|
|
3617
|
-
reason:
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
4194
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4195
|
+
stopReason: "auto_nudge",
|
|
4196
|
+
systemMessage:
|
|
4197
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3621
4198
|
});
|
|
3622
4199
|
} finally {
|
|
3623
4200
|
await rm(cwd, { recursive: true, force: true });
|
|
3624
4201
|
}
|
|
3625
4202
|
});
|
|
3626
4203
|
|
|
3627
|
-
it("
|
|
3628
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
4204
|
+
it("auto-continues native Stop on permission-seeking prompts", async () => {
|
|
4205
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-permission-"));
|
|
3629
4206
|
try {
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
await
|
|
3634
|
-
|
|
3635
|
-
|
|
4207
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4208
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-permission";
|
|
4209
|
+
|
|
4210
|
+
const result = await dispatchCodexNativeHook(
|
|
4211
|
+
{
|
|
4212
|
+
hook_event_name: "Stop",
|
|
4213
|
+
cwd,
|
|
4214
|
+
session_id: "sess-stop-auto-permission",
|
|
4215
|
+
last_assistant_message: "Would you like me to continue with the cleanup?",
|
|
4216
|
+
},
|
|
4217
|
+
{ cwd },
|
|
4218
|
+
);
|
|
4219
|
+
|
|
4220
|
+
assert.equal(result.omxEventName, "stop");
|
|
4221
|
+
assert.deepEqual(result.outputJson, {
|
|
4222
|
+
decision: "block",
|
|
4223
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4224
|
+
stopReason: "auto_nudge",
|
|
4225
|
+
systemMessage:
|
|
4226
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3636
4227
|
});
|
|
3637
|
-
|
|
4228
|
+
} finally {
|
|
4229
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4230
|
+
}
|
|
4231
|
+
});
|
|
4232
|
+
|
|
4233
|
+
it("auto-continues native Stop on \"if you want\" permission-seeking prompts", async () => {
|
|
4234
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-if-you-want-"));
|
|
4235
|
+
try {
|
|
4236
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4237
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-if-you-want";
|
|
4238
|
+
|
|
4239
|
+
const result = await dispatchCodexNativeHook(
|
|
4240
|
+
{
|
|
4241
|
+
hook_event_name: "Stop",
|
|
4242
|
+
cwd,
|
|
4243
|
+
session_id: "sess-stop-auto-if-you-want",
|
|
4244
|
+
last_assistant_message: "If you want, I can continue with the cleanup from here.",
|
|
4245
|
+
},
|
|
4246
|
+
{ cwd },
|
|
4247
|
+
);
|
|
4248
|
+
|
|
4249
|
+
assert.equal(result.omxEventName, "stop");
|
|
4250
|
+
assert.deepEqual(result.outputJson, {
|
|
4251
|
+
decision: "block",
|
|
4252
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4253
|
+
stopReason: "auto_nudge",
|
|
4254
|
+
systemMessage:
|
|
4255
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
4256
|
+
});
|
|
4257
|
+
} finally {
|
|
4258
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4259
|
+
}
|
|
4260
|
+
});
|
|
4261
|
+
|
|
4262
|
+
it("does not auto-continue native Stop while deep-interview is waiting on an intent-first question", async () => {
|
|
4263
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-question-"));
|
|
4264
|
+
try {
|
|
4265
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4266
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-auto-question"), { recursive: true });
|
|
4267
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-question";
|
|
4268
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-question" });
|
|
4269
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "skill-active-state.json"), {
|
|
4270
|
+
version: 1,
|
|
3638
4271
|
active: true,
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
session_id: "
|
|
4272
|
+
skill: "deep-interview",
|
|
4273
|
+
phase: "planning",
|
|
4274
|
+
session_id: "sess-stop-auto-question",
|
|
4275
|
+
thread_id: "thread-stop-auto-question",
|
|
4276
|
+
input_lock: {
|
|
4277
|
+
active: true,
|
|
4278
|
+
scope: "deep-interview-auto-approval",
|
|
4279
|
+
blocked_inputs: ["yes", "proceed"],
|
|
4280
|
+
message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
|
|
4281
|
+
},
|
|
3642
4282
|
});
|
|
3643
|
-
await writeJson(join(stateDir, "
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
transitions: [],
|
|
3648
|
-
updated_at: new Date().toISOString(),
|
|
4283
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "deep-interview-state.json"), {
|
|
4284
|
+
active: true,
|
|
4285
|
+
mode: "deep-interview",
|
|
4286
|
+
current_phase: "intent-first",
|
|
3649
4287
|
});
|
|
3650
4288
|
|
|
3651
|
-
await dispatchCodexNativeHook(
|
|
4289
|
+
const result = await dispatchCodexNativeHook(
|
|
3652
4290
|
{
|
|
3653
4291
|
hook_event_name: "Stop",
|
|
3654
4292
|
cwd,
|
|
3655
|
-
session_id: "
|
|
3656
|
-
thread_id: "thread-stop-
|
|
3657
|
-
turn_id: "turn-stop-
|
|
4293
|
+
session_id: "sess-stop-auto-question",
|
|
4294
|
+
thread_id: "thread-stop-auto-question",
|
|
4295
|
+
turn_id: "turn-stop-auto-question-1",
|
|
4296
|
+
last_assistant_message: [
|
|
4297
|
+
"Round 2 | Target: Decision boundary | Ambiguity: 24%",
|
|
4298
|
+
"",
|
|
4299
|
+
"If an existing project spider still declares session_mode = \"owned\", should ZenX fail loudly so the stale attribute is removed, or should it ignore the attribute and initialize the session pool anyway?",
|
|
4300
|
+
"Keep going once I have your answer.",
|
|
4301
|
+
].join("\n"),
|
|
3658
4302
|
},
|
|
3659
4303
|
{ cwd },
|
|
3660
4304
|
);
|
|
3661
4305
|
|
|
3662
|
-
|
|
4306
|
+
assert.equal(result.omxEventName, "stop");
|
|
4307
|
+
assert.equal(result.outputJson, null);
|
|
4308
|
+
} finally {
|
|
4309
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4312
|
+
|
|
4313
|
+
it("suppresses native auto-nudge re-fire while session-scoped deep-interview state is still active", async () => {
|
|
4314
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-state-"));
|
|
4315
|
+
try {
|
|
4316
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4317
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-auto-interview"), { recursive: true });
|
|
4318
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-interview";
|
|
4319
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-interview" });
|
|
4320
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-interview", "deep-interview-state.json"), {
|
|
4321
|
+
active: true,
|
|
4322
|
+
mode: "deep-interview",
|
|
4323
|
+
current_phase: "intent-first",
|
|
4324
|
+
});
|
|
4325
|
+
|
|
4326
|
+
const result = await dispatchCodexNativeHook(
|
|
3663
4327
|
{
|
|
3664
4328
|
hook_event_name: "Stop",
|
|
3665
4329
|
cwd,
|
|
3666
|
-
session_id: "
|
|
3667
|
-
thread_id: "thread-stop-
|
|
3668
|
-
turn_id: "turn-stop-
|
|
4330
|
+
session_id: "sess-stop-auto-interview",
|
|
4331
|
+
thread_id: "thread-stop-auto-interview",
|
|
4332
|
+
turn_id: "turn-stop-auto-interview-2",
|
|
3669
4333
|
stop_hook_active: true,
|
|
4334
|
+
last_assistant_message: "If you want, I can keep going from here.",
|
|
3670
4335
|
},
|
|
3671
4336
|
{ cwd },
|
|
3672
4337
|
);
|
|
3673
4338
|
|
|
3674
|
-
assert.equal(
|
|
3675
|
-
assert.
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4339
|
+
assert.equal(result.omxEventName, "stop");
|
|
4340
|
+
assert.equal(result.outputJson, null);
|
|
4341
|
+
} finally {
|
|
4342
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4343
|
+
}
|
|
4344
|
+
});
|
|
4345
|
+
|
|
4346
|
+
it("suppresses native auto-nudge when root deep-interview mode state is active without an explicit session", async () => {
|
|
4347
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
|
|
4348
|
+
try {
|
|
4349
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4350
|
+
await mkdir(stateDir, { recursive: true });
|
|
4351
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
|
|
4352
|
+
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
4353
|
+
active: true,
|
|
4354
|
+
mode: "deep-interview",
|
|
4355
|
+
current_phase: "intent-first",
|
|
4356
|
+
});
|
|
4357
|
+
|
|
4358
|
+
const result = await dispatchCodexNativeHook(
|
|
4359
|
+
{
|
|
4360
|
+
hook_event_name: "Stop",
|
|
4361
|
+
cwd,
|
|
4362
|
+
turn_id: "turn-stop-auto-mode-1",
|
|
4363
|
+
last_assistant_message: "Would you like me to continue with the next step?",
|
|
4364
|
+
},
|
|
4365
|
+
{ cwd },
|
|
4366
|
+
);
|
|
4367
|
+
|
|
4368
|
+
assert.equal(result.omxEventName, "stop");
|
|
4369
|
+
assert.equal(result.outputJson, null);
|
|
4370
|
+
} finally {
|
|
4371
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4372
|
+
}
|
|
4373
|
+
});
|
|
4374
|
+
|
|
4375
|
+
it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
|
|
4376
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
|
|
4377
|
+
try {
|
|
4378
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4379
|
+
await mkdir(stateDir, { recursive: true });
|
|
4380
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-mode";
|
|
4381
|
+
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
4382
|
+
active: true,
|
|
4383
|
+
mode: "deep-interview",
|
|
4384
|
+
current_phase: "intent-first",
|
|
3681
4385
|
});
|
|
3682
4386
|
|
|
3683
|
-
const
|
|
4387
|
+
const result = await dispatchCodexNativeHook(
|
|
4388
|
+
{
|
|
4389
|
+
hook_event_name: "Stop",
|
|
4390
|
+
cwd,
|
|
4391
|
+
session_id: "sess-stop-auto-stale-root-mode",
|
|
4392
|
+
thread_id: "thread-stop-auto-stale-root-mode",
|
|
4393
|
+
turn_id: "turn-stop-auto-stale-root-mode-1",
|
|
4394
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
4395
|
+
},
|
|
4396
|
+
{ cwd },
|
|
4397
|
+
);
|
|
4398
|
+
|
|
4399
|
+
assert.equal(result.omxEventName, "stop");
|
|
4400
|
+
assert.deepEqual(result.outputJson, {
|
|
4401
|
+
decision: "block",
|
|
4402
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4403
|
+
stopReason: "auto_nudge",
|
|
4404
|
+
systemMessage:
|
|
4405
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
4406
|
+
});
|
|
4407
|
+
} finally {
|
|
4408
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4409
|
+
}
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
it("does not suppress native auto-nudge from stale root deep-interview skill state when the explicit session-scoped canonical skill state is absent", async () => {
|
|
4413
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-skill-"));
|
|
4414
|
+
try {
|
|
4415
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4416
|
+
await mkdir(stateDir, { recursive: true });
|
|
4417
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-skill";
|
|
4418
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
4419
|
+
active: true,
|
|
4420
|
+
skill: "deep-interview",
|
|
4421
|
+
phase: "planning",
|
|
4422
|
+
});
|
|
4423
|
+
|
|
4424
|
+
const result = await dispatchCodexNativeHook(
|
|
4425
|
+
{
|
|
4426
|
+
hook_event_name: "Stop",
|
|
4427
|
+
cwd,
|
|
4428
|
+
session_id: "sess-stop-auto-stale-root-skill",
|
|
4429
|
+
thread_id: "thread-stop-auto-stale-root-skill",
|
|
4430
|
+
turn_id: "turn-stop-auto-stale-root-skill-1",
|
|
4431
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
4432
|
+
},
|
|
4433
|
+
{ cwd },
|
|
4434
|
+
);
|
|
4435
|
+
|
|
4436
|
+
assert.equal(result.omxEventName, "stop");
|
|
4437
|
+
assert.deepEqual(result.outputJson, {
|
|
4438
|
+
decision: "block",
|
|
4439
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4440
|
+
stopReason: "auto_nudge",
|
|
4441
|
+
systemMessage:
|
|
4442
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
4443
|
+
});
|
|
4444
|
+
} finally {
|
|
4445
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4446
|
+
}
|
|
4447
|
+
});
|
|
4448
|
+
|
|
4449
|
+
it("does not suppress native auto-nudge from stale root deep-interview input lock when the explicit session-scoped canonical skill state is absent", async () => {
|
|
4450
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-lock-"));
|
|
4451
|
+
try {
|
|
4452
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4453
|
+
await mkdir(stateDir, { recursive: true });
|
|
4454
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-lock";
|
|
4455
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
4456
|
+
active: true,
|
|
4457
|
+
skill: "deep-interview",
|
|
4458
|
+
phase: "planning",
|
|
4459
|
+
input_lock: {
|
|
4460
|
+
active: true,
|
|
4461
|
+
scope: "deep-interview-auto-approval",
|
|
4462
|
+
blocked_inputs: ["yes", "proceed"],
|
|
4463
|
+
message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
|
|
4464
|
+
},
|
|
4465
|
+
});
|
|
4466
|
+
|
|
4467
|
+
const result = await dispatchCodexNativeHook(
|
|
4468
|
+
{
|
|
4469
|
+
hook_event_name: "Stop",
|
|
4470
|
+
cwd,
|
|
4471
|
+
session_id: "sess-stop-auto-stale-root-lock",
|
|
4472
|
+
thread_id: "thread-stop-auto-stale-root-lock",
|
|
4473
|
+
turn_id: "turn-stop-auto-stale-root-lock-1",
|
|
4474
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
4475
|
+
},
|
|
4476
|
+
{ cwd },
|
|
4477
|
+
);
|
|
4478
|
+
|
|
4479
|
+
assert.equal(result.omxEventName, "stop");
|
|
4480
|
+
assert.deepEqual(result.outputJson, {
|
|
4481
|
+
decision: "block",
|
|
4482
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4483
|
+
stopReason: "auto_nudge",
|
|
4484
|
+
systemMessage:
|
|
4485
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
4486
|
+
});
|
|
4487
|
+
} finally {
|
|
4488
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4489
|
+
}
|
|
4490
|
+
});
|
|
4491
|
+
|
|
4492
|
+
it("does not suppress native auto-nudge from active root deep-interview state when the current scoped mode state is explicitly inactive", async () => {
|
|
4493
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-inactive-scoped-mode-"));
|
|
4494
|
+
try {
|
|
4495
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4496
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-auto-inactive-mode"), { recursive: true });
|
|
4497
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-inactive-mode";
|
|
4498
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-inactive-mode" });
|
|
4499
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-inactive-mode", "deep-interview-state.json"), {
|
|
4500
|
+
active: false,
|
|
4501
|
+
mode: "deep-interview",
|
|
4502
|
+
current_phase: "completed",
|
|
4503
|
+
});
|
|
4504
|
+
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
4505
|
+
active: true,
|
|
4506
|
+
mode: "deep-interview",
|
|
4507
|
+
current_phase: "intent-first",
|
|
4508
|
+
});
|
|
4509
|
+
|
|
4510
|
+
const result = await dispatchCodexNativeHook(
|
|
4511
|
+
{
|
|
4512
|
+
hook_event_name: "Stop",
|
|
4513
|
+
cwd,
|
|
4514
|
+
session_id: "sess-stop-auto-inactive-mode",
|
|
4515
|
+
thread_id: "thread-stop-auto-inactive-mode",
|
|
4516
|
+
turn_id: "turn-stop-auto-inactive-mode-1",
|
|
4517
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
4518
|
+
},
|
|
4519
|
+
{ cwd },
|
|
4520
|
+
);
|
|
4521
|
+
|
|
4522
|
+
assert.equal(result.omxEventName, "stop");
|
|
4523
|
+
assert.deepEqual(result.outputJson, {
|
|
4524
|
+
decision: "block",
|
|
4525
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4526
|
+
stopReason: "auto_nudge",
|
|
4527
|
+
systemMessage:
|
|
4528
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
4529
|
+
});
|
|
4530
|
+
} finally {
|
|
4531
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
|
|
4535
|
+
it("auto-continues native Stop for permission-seeking prompts even outside OMX runtime", async () => {
|
|
4536
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-plain-session-"));
|
|
4537
|
+
try {
|
|
4538
|
+
await dispatchCodexNativeHook(
|
|
4539
|
+
{
|
|
4540
|
+
hook_event_name: "SessionStart",
|
|
4541
|
+
cwd,
|
|
4542
|
+
session_id: "plain-stop-session",
|
|
4543
|
+
},
|
|
4544
|
+
{
|
|
4545
|
+
cwd,
|
|
4546
|
+
sessionOwnerPid: process.pid,
|
|
4547
|
+
},
|
|
4548
|
+
);
|
|
4549
|
+
|
|
4550
|
+
const result = await dispatchCodexNativeHook(
|
|
4551
|
+
{
|
|
4552
|
+
hook_event_name: "Stop",
|
|
4553
|
+
cwd,
|
|
4554
|
+
session_id: "plain-stop-session",
|
|
4555
|
+
thread_id: "plain-thread",
|
|
4556
|
+
turn_id: "plain-turn-1",
|
|
4557
|
+
last_assistant_message: "If you want, I can continue with the cleanup from here.",
|
|
4558
|
+
},
|
|
4559
|
+
{ cwd },
|
|
4560
|
+
);
|
|
4561
|
+
|
|
4562
|
+
assert.equal(result.omxEventName, "stop");
|
|
4563
|
+
assert.deepEqual(result.outputJson, {
|
|
4564
|
+
decision: "block",
|
|
4565
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
4566
|
+
stopReason: "auto_nudge",
|
|
4567
|
+
systemMessage:
|
|
4568
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
4569
|
+
});
|
|
4570
|
+
} finally {
|
|
4571
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4572
|
+
}
|
|
4573
|
+
});
|
|
4574
|
+
|
|
4575
|
+
it("re-fires team Stop output for a later fresh Stop reply while the team is still active", async () => {
|
|
4576
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-refire-"));
|
|
4577
|
+
try {
|
|
4578
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4579
|
+
await mkdir(stateDir, { recursive: true });
|
|
4580
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
4581
|
+
active: true,
|
|
4582
|
+
current_phase: "team-exec",
|
|
4583
|
+
team_name: "review-team",
|
|
4584
|
+
});
|
|
4585
|
+
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
4586
|
+
current_phase: "team-verify",
|
|
4587
|
+
max_fix_attempts: 3,
|
|
4588
|
+
current_fix_attempt: 0,
|
|
4589
|
+
transitions: [],
|
|
4590
|
+
updated_at: new Date().toISOString(),
|
|
4591
|
+
});
|
|
4592
|
+
|
|
4593
|
+
await dispatchCodexNativeHook(
|
|
4594
|
+
{
|
|
4595
|
+
hook_event_name: "Stop",
|
|
4596
|
+
cwd,
|
|
4597
|
+
session_id: "sess-stop-team-refire",
|
|
4598
|
+
thread_id: "thread-stop-team-refire",
|
|
4599
|
+
turn_id: "turn-stop-team-refire-1",
|
|
4600
|
+
},
|
|
4601
|
+
{ cwd },
|
|
4602
|
+
);
|
|
4603
|
+
|
|
4604
|
+
const result = await dispatchCodexNativeHook(
|
|
4605
|
+
{
|
|
4606
|
+
hook_event_name: "Stop",
|
|
4607
|
+
cwd,
|
|
4608
|
+
session_id: "sess-stop-team-refire",
|
|
4609
|
+
thread_id: "thread-stop-team-refire",
|
|
4610
|
+
turn_id: "turn-stop-team-refire-2",
|
|
4611
|
+
stop_hook_active: true,
|
|
4612
|
+
},
|
|
4613
|
+
{ cwd },
|
|
4614
|
+
);
|
|
4615
|
+
|
|
4616
|
+
assert.equal(result.omxEventName, "stop");
|
|
4617
|
+
assert.deepEqual(result.outputJson, {
|
|
4618
|
+
decision: "block",
|
|
4619
|
+
reason:
|
|
4620
|
+
`OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
4621
|
+
stopReason: "team_team-verify",
|
|
4622
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
4623
|
+
});
|
|
4624
|
+
} finally {
|
|
4625
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4626
|
+
}
|
|
4627
|
+
});
|
|
4628
|
+
|
|
4629
|
+
it("suppresses duplicate team Stop replays across native/canonical session-id drift", async () => {
|
|
4630
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-drift-"));
|
|
4631
|
+
try {
|
|
4632
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4633
|
+
await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
|
|
4634
|
+
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
4635
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
4636
|
+
session_id: "omx-canonical",
|
|
4637
|
+
native_session_id: "codex-native",
|
|
4638
|
+
});
|
|
4639
|
+
await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
|
|
4640
|
+
active: true,
|
|
4641
|
+
current_phase: "starting",
|
|
4642
|
+
team_name: "current-team",
|
|
4643
|
+
session_id: "omx-canonical",
|
|
4644
|
+
});
|
|
4645
|
+
await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
|
|
4646
|
+
current_phase: "team-verify",
|
|
4647
|
+
max_fix_attempts: 3,
|
|
4648
|
+
current_fix_attempt: 1,
|
|
4649
|
+
transitions: [],
|
|
4650
|
+
updated_at: new Date().toISOString(),
|
|
4651
|
+
});
|
|
4652
|
+
|
|
4653
|
+
await dispatchCodexNativeHook(
|
|
4654
|
+
{
|
|
4655
|
+
hook_event_name: "Stop",
|
|
4656
|
+
cwd,
|
|
4657
|
+
session_id: "codex-native",
|
|
4658
|
+
thread_id: "thread-stop-team-drift",
|
|
4659
|
+
turn_id: "turn-stop-team-drift-1",
|
|
4660
|
+
},
|
|
4661
|
+
{ cwd },
|
|
4662
|
+
);
|
|
4663
|
+
|
|
4664
|
+
const duplicate = await dispatchCodexNativeHook(
|
|
4665
|
+
{
|
|
4666
|
+
hook_event_name: "Stop",
|
|
4667
|
+
cwd,
|
|
4668
|
+
session_id: "omx-canonical",
|
|
4669
|
+
thread_id: "thread-stop-team-drift",
|
|
4670
|
+
turn_id: "turn-stop-team-drift-1",
|
|
4671
|
+
stop_hook_active: true,
|
|
4672
|
+
},
|
|
4673
|
+
{ cwd },
|
|
4674
|
+
);
|
|
4675
|
+
|
|
4676
|
+
assert.equal(duplicate.omxEventName, "stop");
|
|
4677
|
+
assert.deepEqual(duplicate.outputJson, {
|
|
4678
|
+
decision: "block",
|
|
4679
|
+
reason:
|
|
4680
|
+
`OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
4681
|
+
stopReason: "team_team-verify",
|
|
4682
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
4683
|
+
});
|
|
4684
|
+
|
|
4685
|
+
const fresh = await dispatchCodexNativeHook(
|
|
4686
|
+
{
|
|
4687
|
+
hook_event_name: "Stop",
|
|
4688
|
+
cwd,
|
|
4689
|
+
session_id: "omx-canonical",
|
|
4690
|
+
thread_id: "thread-stop-team-drift",
|
|
4691
|
+
turn_id: "turn-stop-team-drift-2",
|
|
4692
|
+
stop_hook_active: true,
|
|
4693
|
+
},
|
|
4694
|
+
{ cwd },
|
|
4695
|
+
);
|
|
4696
|
+
|
|
4697
|
+
assert.equal(fresh.omxEventName, "stop");
|
|
4698
|
+
assert.deepEqual(fresh.outputJson, {
|
|
4699
|
+
decision: "block",
|
|
4700
|
+
reason:
|
|
4701
|
+
`OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
4702
|
+
stopReason: "team_team-verify",
|
|
4703
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
4704
|
+
});
|
|
4705
|
+
|
|
4706
|
+
const persisted = JSON.parse(
|
|
4707
|
+
await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
|
|
4708
|
+
) as { sessions?: Record<string, unknown> };
|
|
4709
|
+
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
4710
|
+
} finally {
|
|
4711
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4712
|
+
}
|
|
4713
|
+
});
|
|
4714
|
+
|
|
4715
|
+
it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
|
|
4716
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
|
|
4717
|
+
try {
|
|
4718
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4719
|
+
await mkdir(stateDir, { recursive: true });
|
|
4720
|
+
await writeJson(join(stateDir, "ultrawork-state.json"), {
|
|
4721
|
+
active: true,
|
|
4722
|
+
current_phase: "executing",
|
|
4723
|
+
});
|
|
4724
|
+
|
|
4725
|
+
const first = await dispatchCodexNativeHook(
|
|
4726
|
+
{
|
|
4727
|
+
hook_event_name: "Stop",
|
|
4728
|
+
cwd,
|
|
4729
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
4730
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
4731
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
4732
|
+
},
|
|
4733
|
+
{ cwd },
|
|
4734
|
+
);
|
|
4735
|
+
|
|
4736
|
+
const repeated = await dispatchCodexNativeHook(
|
|
4737
|
+
{
|
|
4738
|
+
hook_event_name: "Stop",
|
|
4739
|
+
cwd,
|
|
4740
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
4741
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
4742
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
4743
|
+
stop_hook_active: true,
|
|
4744
|
+
},
|
|
4745
|
+
{ cwd },
|
|
4746
|
+
);
|
|
4747
|
+
|
|
4748
|
+
const fresh = await dispatchCodexNativeHook(
|
|
4749
|
+
{
|
|
4750
|
+
hook_event_name: "Stop",
|
|
4751
|
+
cwd,
|
|
4752
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
4753
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
4754
|
+
turn_id: "turn-stop-ultrawork-repeat-2",
|
|
4755
|
+
stop_hook_active: true,
|
|
4756
|
+
},
|
|
4757
|
+
{ cwd },
|
|
4758
|
+
);
|
|
4759
|
+
|
|
4760
|
+
assert.equal(first.omxEventName, "stop");
|
|
4761
|
+
assert.deepEqual(repeated.outputJson, null);
|
|
4762
|
+
assert.equal(fresh.omxEventName, "stop");
|
|
4763
|
+
assert.deepEqual(fresh.outputJson, {
|
|
4764
|
+
decision: "block",
|
|
4765
|
+
reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
4766
|
+
stopReason: "ultrawork_executing",
|
|
4767
|
+
systemMessage: "OMX ultrawork is still active (phase: executing).",
|
|
4768
|
+
});
|
|
4769
|
+
} finally {
|
|
4770
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4771
|
+
}
|
|
4772
|
+
});
|
|
4773
|
+
|
|
4774
|
+
it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
|
|
4775
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
|
|
4776
|
+
try {
|
|
4777
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4778
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
|
|
4779
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
|
|
4780
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
|
|
4781
|
+
active: true,
|
|
4782
|
+
skill: "ralplan",
|
|
4783
|
+
phase: "planning",
|
|
4784
|
+
});
|
|
4785
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
|
|
4786
|
+
active: true,
|
|
4787
|
+
current_phase: "planning",
|
|
4788
|
+
});
|
|
4789
|
+
|
|
4790
|
+
await dispatchCodexNativeHook(
|
|
4791
|
+
{
|
|
4792
|
+
hook_event_name: "Stop",
|
|
4793
|
+
cwd,
|
|
4794
|
+
session_id: "sess-stop-skill-repeat",
|
|
4795
|
+
thread_id: "thread-stop-skill-repeat",
|
|
4796
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
4797
|
+
},
|
|
4798
|
+
{ cwd },
|
|
4799
|
+
);
|
|
4800
|
+
|
|
4801
|
+
const repeated = await dispatchCodexNativeHook(
|
|
4802
|
+
{
|
|
4803
|
+
hook_event_name: "Stop",
|
|
4804
|
+
cwd,
|
|
4805
|
+
session_id: "sess-stop-skill-repeat",
|
|
4806
|
+
thread_id: "thread-stop-skill-repeat",
|
|
4807
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
4808
|
+
stop_hook_active: true,
|
|
4809
|
+
},
|
|
4810
|
+
{ cwd },
|
|
4811
|
+
);
|
|
4812
|
+
|
|
4813
|
+
assert.equal(repeated.omxEventName, "stop");
|
|
4814
|
+
assert.deepEqual(repeated.outputJson, {
|
|
4815
|
+
decision: "block",
|
|
4816
|
+
reason:
|
|
4817
|
+
"OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
|
|
4818
|
+
stopReason: "skill_ralplan_planning",
|
|
4819
|
+
systemMessage: "OMX skill ralplan is still active (phase: planning).",
|
|
4820
|
+
});
|
|
4821
|
+
} finally {
|
|
4822
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4823
|
+
}
|
|
4824
|
+
});
|
|
4825
|
+
|
|
4826
|
+
it("does not block Stop from another session's stale root team state when no scoped team state exists", async () => {
|
|
4827
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-team-"));
|
|
4828
|
+
try {
|
|
4829
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4830
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
4831
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
4832
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
4833
|
+
active: true,
|
|
4834
|
+
current_phase: "starting",
|
|
4835
|
+
team_name: "stale-root-team",
|
|
4836
|
+
session_id: "sess-other",
|
|
4837
|
+
});
|
|
4838
|
+
await writeJson(join(stateDir, "team", "stale-root-team", "phase.json"), {
|
|
4839
|
+
current_phase: "team-exec",
|
|
4840
|
+
max_fix_attempts: 3,
|
|
4841
|
+
current_fix_attempt: 0,
|
|
4842
|
+
transitions: [],
|
|
4843
|
+
updated_at: new Date().toISOString(),
|
|
4844
|
+
});
|
|
4845
|
+
|
|
4846
|
+
const result = await dispatchCodexNativeHook(
|
|
4847
|
+
{
|
|
4848
|
+
hook_event_name: "Stop",
|
|
4849
|
+
cwd,
|
|
4850
|
+
session_id: "sess-current",
|
|
4851
|
+
},
|
|
4852
|
+
{ cwd },
|
|
4853
|
+
);
|
|
4854
|
+
|
|
4855
|
+
assert.equal(result.omxEventName, "stop");
|
|
4856
|
+
assert.equal(result.outputJson, null);
|
|
4857
|
+
} finally {
|
|
4858
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4859
|
+
}
|
|
4860
|
+
});
|
|
4861
|
+
|
|
4862
|
+
it("does not block Stop from orphaned team mode state after cleanup removed canonical team artifacts", async () => {
|
|
4863
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-orphaned-team-state-"));
|
|
4864
|
+
try {
|
|
4865
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4866
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
4867
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
4868
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
4869
|
+
active: true,
|
|
4870
|
+
current_phase: "starting",
|
|
4871
|
+
team_name: "cleaned-team",
|
|
4872
|
+
session_id: "sess-current",
|
|
4873
|
+
});
|
|
4874
|
+
|
|
4875
|
+
const result = await dispatchCodexNativeHook(
|
|
4876
|
+
{
|
|
4877
|
+
hook_event_name: "Stop",
|
|
4878
|
+
cwd,
|
|
4879
|
+
session_id: "sess-current",
|
|
4880
|
+
},
|
|
4881
|
+
{ cwd },
|
|
4882
|
+
);
|
|
4883
|
+
|
|
4884
|
+
assert.equal(result.omxEventName, "stop");
|
|
4885
|
+
assert.equal(result.outputJson, null);
|
|
4886
|
+
} finally {
|
|
4887
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4888
|
+
}
|
|
4889
|
+
});
|
|
4890
|
+
|
|
4891
|
+
it("prefers the current session team state over a stale root team fallback during Stop", async () => {
|
|
4892
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-current-session-team-preferred-"));
|
|
4893
|
+
try {
|
|
4894
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4895
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
4896
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
4897
|
+
await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
|
|
4898
|
+
active: true,
|
|
4899
|
+
current_phase: "starting",
|
|
4900
|
+
team_name: "current-team",
|
|
4901
|
+
session_id: "sess-current",
|
|
4902
|
+
});
|
|
4903
|
+
await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
|
|
4904
|
+
current_phase: "team-verify",
|
|
4905
|
+
max_fix_attempts: 3,
|
|
4906
|
+
current_fix_attempt: 1,
|
|
4907
|
+
transitions: [],
|
|
4908
|
+
updated_at: new Date().toISOString(),
|
|
4909
|
+
});
|
|
4910
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
4911
|
+
active: true,
|
|
4912
|
+
current_phase: "starting",
|
|
4913
|
+
team_name: "stale-root-team",
|
|
4914
|
+
session_id: "sess-other",
|
|
4915
|
+
});
|
|
4916
|
+
await writeJson(join(stateDir, "team", "stale-root-team", "phase.json"), {
|
|
4917
|
+
current_phase: "team-exec",
|
|
4918
|
+
max_fix_attempts: 3,
|
|
4919
|
+
current_fix_attempt: 0,
|
|
4920
|
+
transitions: [],
|
|
4921
|
+
updated_at: new Date().toISOString(),
|
|
4922
|
+
});
|
|
4923
|
+
|
|
4924
|
+
const result = await dispatchCodexNativeHook(
|
|
4925
|
+
{
|
|
4926
|
+
hook_event_name: "Stop",
|
|
4927
|
+
cwd,
|
|
4928
|
+
session_id: "sess-current",
|
|
4929
|
+
},
|
|
4930
|
+
{ cwd },
|
|
4931
|
+
);
|
|
4932
|
+
|
|
4933
|
+
assert.equal(result.omxEventName, "stop");
|
|
4934
|
+
assert.deepEqual(result.outputJson, {
|
|
4935
|
+
decision: "block",
|
|
4936
|
+
reason:
|
|
4937
|
+
`OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
4938
|
+
stopReason: "team_team-verify",
|
|
4939
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
4940
|
+
});
|
|
4941
|
+
} finally {
|
|
4942
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4943
|
+
}
|
|
4944
|
+
});
|
|
4945
|
+
|
|
4946
|
+
it("does not fall back to active root team state when the current scoped team state is inactive", async () => {
|
|
4947
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-inactive-scoped-team-"));
|
|
4948
|
+
try {
|
|
4949
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4950
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
4951
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
4952
|
+
await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
|
|
4953
|
+
active: false,
|
|
4954
|
+
current_phase: "complete",
|
|
4955
|
+
team_name: "scoped-finished-team",
|
|
4956
|
+
session_id: "sess-current",
|
|
4957
|
+
});
|
|
4958
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
4959
|
+
active: true,
|
|
4960
|
+
current_phase: "starting",
|
|
4961
|
+
team_name: "root-fallback-team",
|
|
4962
|
+
session_id: "sess-current",
|
|
4963
|
+
});
|
|
4964
|
+
await writeJson(join(stateDir, "team", "root-fallback-team", "phase.json"), {
|
|
4965
|
+
current_phase: "team-exec",
|
|
4966
|
+
max_fix_attempts: 3,
|
|
4967
|
+
current_fix_attempt: 0,
|
|
4968
|
+
transitions: [],
|
|
4969
|
+
updated_at: new Date().toISOString(),
|
|
4970
|
+
});
|
|
4971
|
+
|
|
4972
|
+
const result = await dispatchCodexNativeHook(
|
|
4973
|
+
{
|
|
4974
|
+
hook_event_name: "Stop",
|
|
4975
|
+
cwd,
|
|
4976
|
+
session_id: "sess-current",
|
|
4977
|
+
},
|
|
4978
|
+
{ cwd },
|
|
4979
|
+
);
|
|
4980
|
+
|
|
4981
|
+
assert.equal(result.omxEventName, "stop");
|
|
4982
|
+
assert.equal(result.outputJson, null);
|
|
4983
|
+
} finally {
|
|
4984
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4985
|
+
}
|
|
4986
|
+
});
|
|
4987
|
+
});
|
|
4988
|
+
|
|
4989
|
+
// ---------------------------------------------------------------------------
|
|
4990
|
+
// Triage layer integration tests
|
|
4991
|
+
// ---------------------------------------------------------------------------
|
|
4992
|
+
|
|
4993
|
+
describe("codex native hook triage integration", () => {
|
|
4994
|
+
const priorCodexHome = process.env.CODEX_HOME;
|
|
4995
|
+
|
|
4996
|
+
beforeEach(() => {
|
|
4997
|
+
resetTriageConfigCache();
|
|
4998
|
+
});
|
|
4999
|
+
|
|
5000
|
+
afterEach(() => {
|
|
5001
|
+
if (typeof priorCodexHome === "string") process.env.CODEX_HOME = priorCodexHome;
|
|
5002
|
+
else delete process.env.CODEX_HOME;
|
|
5003
|
+
resetTriageConfigCache();
|
|
5004
|
+
});
|
|
5005
|
+
|
|
5006
|
+
// ── Group 1: Keyword bypass (triage must NOT run) ────────────────────────
|
|
5007
|
+
|
|
5008
|
+
it("does not inject triage advisory for $ralplan keyword prompts", async () => {
|
|
5009
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-ralplan-"));
|
|
5010
|
+
try {
|
|
5011
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5012
|
+
const result = await dispatchCodexNativeHook(
|
|
5013
|
+
{
|
|
5014
|
+
hook_event_name: "UserPromptSubmit",
|
|
5015
|
+
cwd,
|
|
5016
|
+
session_id: "triage-kw-ralplan-1",
|
|
5017
|
+
thread_id: "thread-triage-kw-1",
|
|
5018
|
+
turn_id: "turn-triage-kw-1",
|
|
5019
|
+
prompt: "$ralplan implement issue #1307",
|
|
5020
|
+
},
|
|
5021
|
+
{ cwd },
|
|
5022
|
+
);
|
|
5023
|
+
|
|
5024
|
+
const additionalContext = String(
|
|
5025
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5026
|
+
);
|
|
5027
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5028
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
5029
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
5030
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
5031
|
+
|
|
5032
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-ralplan-1", "prompt-routing-state.json");
|
|
5033
|
+
assert.equal(existsSync(stateFile), false);
|
|
5034
|
+
} finally {
|
|
5035
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5036
|
+
}
|
|
5037
|
+
});
|
|
5038
|
+
|
|
5039
|
+
it("does not inject triage advisory for autopilot keyword prompts", async () => {
|
|
5040
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-autopilot-"));
|
|
5041
|
+
try {
|
|
5042
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5043
|
+
const result = await dispatchCodexNativeHook(
|
|
5044
|
+
{
|
|
5045
|
+
hook_event_name: "UserPromptSubmit",
|
|
5046
|
+
cwd,
|
|
5047
|
+
session_id: "triage-kw-autopilot-1",
|
|
5048
|
+
thread_id: "thread-triage-kw-ap-1",
|
|
5049
|
+
turn_id: "turn-triage-kw-ap-1",
|
|
5050
|
+
prompt: "$autopilot build this",
|
|
5051
|
+
},
|
|
5052
|
+
{ cwd },
|
|
5053
|
+
);
|
|
5054
|
+
|
|
5055
|
+
const additionalContext = String(
|
|
5056
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5057
|
+
);
|
|
5058
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5059
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
5060
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
5061
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
5062
|
+
|
|
5063
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-autopilot-1", "prompt-routing-state.json");
|
|
5064
|
+
assert.equal(existsSync(stateFile), false);
|
|
5065
|
+
} finally {
|
|
5066
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5067
|
+
}
|
|
5068
|
+
});
|
|
5069
|
+
|
|
5070
|
+
// ── Group 2: HEAVY injection ─────────────────────────────────────────────
|
|
5071
|
+
|
|
5072
|
+
it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
|
|
5073
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));
|
|
5074
|
+
try {
|
|
5075
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5076
|
+
const result = await dispatchCodexNativeHook(
|
|
5077
|
+
{
|
|
5078
|
+
hook_event_name: "UserPromptSubmit",
|
|
5079
|
+
cwd,
|
|
5080
|
+
session_id: "triage-heavy-1",
|
|
5081
|
+
thread_id: "thread-triage-heavy-1",
|
|
5082
|
+
turn_id: "turn-triage-heavy-1",
|
|
5083
|
+
prompt: "add dark mode toggle to the settings page",
|
|
5084
|
+
},
|
|
5085
|
+
{ cwd },
|
|
5086
|
+
);
|
|
5087
|
+
|
|
5088
|
+
const additionalContext = String(
|
|
5089
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5090
|
+
);
|
|
5091
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5092
|
+
assert.match(additionalContext, /Prefer the existing autopilot-style workflow/);
|
|
5093
|
+
|
|
5094
|
+
// skill-active-state.json must NOT be written (triage is advisory only)
|
|
5095
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
5096
|
+
|
|
5097
|
+
// prompt-routing-state.json must be written with lane=HEAVY
|
|
5098
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-heavy-1", "prompt-routing-state.json");
|
|
5099
|
+
assert.equal(existsSync(stateFile), true);
|
|
5100
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
5101
|
+
version?: number;
|
|
5102
|
+
last_triage?: { lane?: string; destination?: string };
|
|
5103
|
+
suppress_followup?: boolean;
|
|
5104
|
+
};
|
|
5105
|
+
assert.equal(state.version, 1);
|
|
5106
|
+
assert.equal(state.last_triage?.lane, "HEAVY");
|
|
5107
|
+
assert.equal(state.last_triage?.destination, "autopilot");
|
|
5108
|
+
assert.equal(state.suppress_followup, true);
|
|
5109
|
+
} finally {
|
|
5110
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5111
|
+
}
|
|
5112
|
+
});
|
|
5113
|
+
|
|
5114
|
+
// ── Group 3: LIGHT/explore ────────────────────────────────────────────────
|
|
5115
|
+
|
|
5116
|
+
it("injects LIGHT/explore advisory and writes state for a question-shaped prompt", async () => {
|
|
5117
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-explore-"));
|
|
5118
|
+
try {
|
|
5119
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5120
|
+
const result = await dispatchCodexNativeHook(
|
|
5121
|
+
{
|
|
5122
|
+
hook_event_name: "UserPromptSubmit",
|
|
5123
|
+
cwd,
|
|
5124
|
+
session_id: "triage-explore-1",
|
|
5125
|
+
thread_id: "thread-triage-explore-1",
|
|
5126
|
+
turn_id: "turn-triage-explore-1",
|
|
5127
|
+
prompt: "explain this function",
|
|
5128
|
+
},
|
|
5129
|
+
{ cwd },
|
|
5130
|
+
);
|
|
5131
|
+
|
|
5132
|
+
const additionalContext = String(
|
|
5133
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5134
|
+
);
|
|
5135
|
+
assert.match(additionalContext, /read-only\/question-shaped/);
|
|
5136
|
+
assert.match(additionalContext, /Prefer the explore role surface/);
|
|
5137
|
+
|
|
5138
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-explore-1", "prompt-routing-state.json");
|
|
5139
|
+
assert.equal(existsSync(stateFile), true);
|
|
5140
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
5141
|
+
last_triage?: { lane?: string; destination?: string };
|
|
5142
|
+
suppress_followup?: boolean;
|
|
5143
|
+
};
|
|
5144
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
5145
|
+
assert.equal(state.last_triage?.destination, "explore");
|
|
5146
|
+
assert.equal(state.suppress_followup, true);
|
|
5147
|
+
} finally {
|
|
5148
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5149
|
+
}
|
|
5150
|
+
});
|
|
5151
|
+
|
|
5152
|
+
// ── Group 4: LIGHT/executor ───────────────────────────────────────────────
|
|
5153
|
+
|
|
5154
|
+
it("injects LIGHT/executor advisory and writes state for a narrow edit-shaped prompt", async () => {
|
|
5155
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-executor-"));
|
|
5156
|
+
try {
|
|
5157
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5158
|
+
const result = await dispatchCodexNativeHook(
|
|
5159
|
+
{
|
|
5160
|
+
hook_event_name: "UserPromptSubmit",
|
|
5161
|
+
cwd,
|
|
5162
|
+
session_id: "triage-executor-1",
|
|
5163
|
+
thread_id: "thread-triage-executor-1",
|
|
5164
|
+
turn_id: "turn-triage-executor-1",
|
|
5165
|
+
prompt: "fix typo in src/foo.ts",
|
|
5166
|
+
},
|
|
5167
|
+
{ cwd },
|
|
5168
|
+
);
|
|
5169
|
+
|
|
5170
|
+
const additionalContext = String(
|
|
5171
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5172
|
+
);
|
|
5173
|
+
assert.match(additionalContext, /narrow edit-shaped/);
|
|
5174
|
+
assert.match(additionalContext, /Prefer the executor role surface/);
|
|
5175
|
+
|
|
5176
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-executor-1", "prompt-routing-state.json");
|
|
5177
|
+
assert.equal(existsSync(stateFile), true);
|
|
5178
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
5179
|
+
last_triage?: { lane?: string; destination?: string };
|
|
5180
|
+
};
|
|
5181
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
5182
|
+
assert.equal(state.last_triage?.destination, "executor");
|
|
5183
|
+
} finally {
|
|
5184
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5185
|
+
}
|
|
5186
|
+
});
|
|
5187
|
+
|
|
5188
|
+
// ── Group 5: LIGHT/designer ───────────────────────────────────────────────
|
|
5189
|
+
|
|
5190
|
+
it("injects LIGHT/designer advisory and writes state for a visual/style prompt", async () => {
|
|
5191
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-designer-"));
|
|
5192
|
+
try {
|
|
5193
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5194
|
+
const result = await dispatchCodexNativeHook(
|
|
5195
|
+
{
|
|
5196
|
+
hook_event_name: "UserPromptSubmit",
|
|
5197
|
+
cwd,
|
|
5198
|
+
session_id: "triage-designer-1",
|
|
5199
|
+
thread_id: "thread-triage-designer-1",
|
|
5200
|
+
turn_id: "turn-triage-designer-1",
|
|
5201
|
+
prompt: "make the button blue",
|
|
5202
|
+
},
|
|
5203
|
+
{ cwd },
|
|
5204
|
+
);
|
|
5205
|
+
|
|
5206
|
+
const additionalContext = String(
|
|
5207
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5208
|
+
);
|
|
5209
|
+
assert.match(additionalContext, /visual\/style request/);
|
|
5210
|
+
assert.match(additionalContext, /Prefer the designer role surface/);
|
|
5211
|
+
|
|
5212
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-designer-1", "prompt-routing-state.json");
|
|
5213
|
+
assert.equal(existsSync(stateFile), true);
|
|
5214
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
5215
|
+
last_triage?: { lane?: string; destination?: string };
|
|
5216
|
+
};
|
|
5217
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
5218
|
+
assert.equal(state.last_triage?.destination, "designer");
|
|
5219
|
+
} finally {
|
|
5220
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5221
|
+
}
|
|
5222
|
+
});
|
|
5223
|
+
|
|
5224
|
+
// ── Group 6: PASS (no triage injection, no state) ────────────────────────
|
|
5225
|
+
|
|
5226
|
+
it("produces no triage advisory and no state for trivial greeting prompts", async () => {
|
|
5227
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-hello-"));
|
|
5228
|
+
try {
|
|
5229
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5230
|
+
const result = await dispatchCodexNativeHook(
|
|
5231
|
+
{
|
|
5232
|
+
hook_event_name: "UserPromptSubmit",
|
|
5233
|
+
cwd,
|
|
5234
|
+
session_id: "triage-pass-hello-1",
|
|
5235
|
+
thread_id: "thread-triage-pass-1",
|
|
5236
|
+
turn_id: "turn-triage-pass-1",
|
|
5237
|
+
prompt: "hello",
|
|
5238
|
+
},
|
|
5239
|
+
{ cwd },
|
|
5240
|
+
);
|
|
5241
|
+
|
|
5242
|
+
const additionalContext = String(
|
|
5243
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5244
|
+
);
|
|
5245
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5246
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
5247
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
5248
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
5249
|
+
|
|
5250
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-hello-1", "prompt-routing-state.json");
|
|
5251
|
+
assert.equal(existsSync(stateFile), false);
|
|
5252
|
+
} finally {
|
|
5253
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5254
|
+
}
|
|
5255
|
+
});
|
|
5256
|
+
|
|
5257
|
+
it("produces no triage advisory and no state for ambiguous short prompts", async () => {
|
|
5258
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-short-"));
|
|
5259
|
+
try {
|
|
5260
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5261
|
+
const result = await dispatchCodexNativeHook(
|
|
5262
|
+
{
|
|
5263
|
+
hook_event_name: "UserPromptSubmit",
|
|
5264
|
+
cwd,
|
|
5265
|
+
session_id: "triage-pass-short-1",
|
|
5266
|
+
thread_id: "thread-triage-pass-short-1",
|
|
5267
|
+
turn_id: "turn-triage-pass-short-1",
|
|
5268
|
+
prompt: "fix the thing",
|
|
5269
|
+
},
|
|
5270
|
+
{ cwd },
|
|
5271
|
+
);
|
|
5272
|
+
|
|
5273
|
+
const additionalContext = String(
|
|
5274
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5275
|
+
);
|
|
5276
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5277
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
5278
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
5279
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
5280
|
+
|
|
5281
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-short-1", "prompt-routing-state.json");
|
|
5282
|
+
assert.equal(existsSync(stateFile), false);
|
|
5283
|
+
} finally {
|
|
5284
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5285
|
+
}
|
|
5286
|
+
});
|
|
5287
|
+
|
|
5288
|
+
// ── Group 7: Turn-2 suppression (same session across two invocations) ────
|
|
5289
|
+
|
|
5290
|
+
it("suppresses HEAVY triage re-injection on a short follow-up in the same session", async () => {
|
|
5291
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-heavy-"));
|
|
5292
|
+
const sessionId = "triage-suppress-heavy-1";
|
|
5293
|
+
try {
|
|
5294
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5295
|
+
|
|
5296
|
+
// Turn 1: HEAVY fires
|
|
5297
|
+
const turn1 = await dispatchCodexNativeHook(
|
|
5298
|
+
{
|
|
5299
|
+
hook_event_name: "UserPromptSubmit",
|
|
5300
|
+
cwd,
|
|
5301
|
+
session_id: sessionId,
|
|
5302
|
+
thread_id: "thread-suppress-heavy-1",
|
|
5303
|
+
turn_id: "turn-suppress-heavy-1",
|
|
5304
|
+
prompt: "add dark mode toggle to the settings page",
|
|
5305
|
+
},
|
|
5306
|
+
{ cwd },
|
|
5307
|
+
);
|
|
5308
|
+
const ctx1 = String(
|
|
5309
|
+
(turn1.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5310
|
+
);
|
|
5311
|
+
assert.match(ctx1, /multi-step goal with no workflow keyword/);
|
|
5312
|
+
|
|
5313
|
+
// Turn 2: short follow-up — triage suppressed
|
|
5314
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
5315
|
+
{
|
|
5316
|
+
hook_event_name: "UserPromptSubmit",
|
|
5317
|
+
cwd,
|
|
5318
|
+
session_id: sessionId,
|
|
5319
|
+
thread_id: "thread-suppress-heavy-1",
|
|
5320
|
+
turn_id: "turn-suppress-heavy-2",
|
|
5321
|
+
prompt: "yes, settings page",
|
|
5322
|
+
},
|
|
5323
|
+
{ cwd },
|
|
5324
|
+
);
|
|
5325
|
+
const ctx2 = String(
|
|
5326
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5327
|
+
);
|
|
5328
|
+
assert.doesNotMatch(ctx2, /multi-step goal/);
|
|
5329
|
+
} finally {
|
|
5330
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5331
|
+
}
|
|
5332
|
+
});
|
|
5333
|
+
|
|
5334
|
+
it("suppresses LIGHT/explore triage re-injection on a short follow-up in the same session", async () => {
|
|
5335
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-explore-"));
|
|
5336
|
+
const sessionId = "triage-suppress-explore-1";
|
|
5337
|
+
try {
|
|
5338
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5339
|
+
|
|
5340
|
+
// Turn 1: LIGHT/explore fires
|
|
5341
|
+
await dispatchCodexNativeHook(
|
|
5342
|
+
{
|
|
5343
|
+
hook_event_name: "UserPromptSubmit",
|
|
5344
|
+
cwd,
|
|
5345
|
+
session_id: sessionId,
|
|
5346
|
+
thread_id: "thread-suppress-explore-1",
|
|
5347
|
+
turn_id: "turn-suppress-explore-1",
|
|
5348
|
+
prompt: "explain this function",
|
|
5349
|
+
},
|
|
5350
|
+
{ cwd },
|
|
5351
|
+
);
|
|
5352
|
+
|
|
5353
|
+
// Turn 2: short follow-up — no duplicate LIGHT injection
|
|
5354
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
5355
|
+
{
|
|
5356
|
+
hook_event_name: "UserPromptSubmit",
|
|
5357
|
+
cwd,
|
|
5358
|
+
session_id: sessionId,
|
|
5359
|
+
thread_id: "thread-suppress-explore-1",
|
|
5360
|
+
turn_id: "turn-suppress-explore-2",
|
|
5361
|
+
prompt: "the auth helper",
|
|
5362
|
+
},
|
|
5363
|
+
{ cwd },
|
|
5364
|
+
);
|
|
5365
|
+
const ctx2 = String(
|
|
5366
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5367
|
+
);
|
|
5368
|
+
assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
|
|
5369
|
+
} finally {
|
|
5370
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5371
|
+
}
|
|
5372
|
+
});
|
|
5373
|
+
|
|
5374
|
+
// ── Group 8: First-turn PASS does NOT block later triage ─────────────────
|
|
5375
|
+
|
|
5376
|
+
it("still applies triage on turn 2 when turn 1 was a PASS with no state written", async () => {
|
|
5377
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-then-light-"));
|
|
5378
|
+
const sessionId = "triage-pass-then-light-1";
|
|
5379
|
+
try {
|
|
5380
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5381
|
+
|
|
5382
|
+
// Turn 1: PASS — no state written
|
|
5383
|
+
await dispatchCodexNativeHook(
|
|
5384
|
+
{
|
|
5385
|
+
hook_event_name: "UserPromptSubmit",
|
|
5386
|
+
cwd,
|
|
5387
|
+
session_id: sessionId,
|
|
5388
|
+
thread_id: "thread-pass-then-light-1",
|
|
5389
|
+
turn_id: "turn-pass-then-light-1",
|
|
5390
|
+
prompt: "hello",
|
|
5391
|
+
},
|
|
5392
|
+
{ cwd },
|
|
5393
|
+
);
|
|
5394
|
+
assert.equal(
|
|
5395
|
+
existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json")),
|
|
5396
|
+
false,
|
|
5397
|
+
);
|
|
5398
|
+
|
|
5399
|
+
// Turn 2: LIGHT/executor should fire normally
|
|
5400
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
5401
|
+
{
|
|
5402
|
+
hook_event_name: "UserPromptSubmit",
|
|
5403
|
+
cwd,
|
|
5404
|
+
session_id: sessionId,
|
|
5405
|
+
thread_id: "thread-pass-then-light-1",
|
|
5406
|
+
turn_id: "turn-pass-then-light-2",
|
|
5407
|
+
prompt: "fix typo in src/foo.ts",
|
|
5408
|
+
},
|
|
5409
|
+
{ cwd },
|
|
5410
|
+
);
|
|
5411
|
+
const ctx2 = String(
|
|
5412
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5413
|
+
);
|
|
5414
|
+
assert.match(ctx2, /narrow edit-shaped/);
|
|
5415
|
+
} finally {
|
|
5416
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5417
|
+
}
|
|
5418
|
+
});
|
|
5419
|
+
|
|
5420
|
+
// ── Group 9: Opt-out forces PASS ─────────────────────────────────────────
|
|
5421
|
+
|
|
5422
|
+
it("produces no triage advisory when prompt contains 'just chat' opt-out", async () => {
|
|
5423
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-chat-"));
|
|
5424
|
+
try {
|
|
5425
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5426
|
+
const result = await dispatchCodexNativeHook(
|
|
3684
5427
|
{
|
|
3685
|
-
hook_event_name: "
|
|
5428
|
+
hook_event_name: "UserPromptSubmit",
|
|
3686
5429
|
cwd,
|
|
3687
|
-
session_id: "
|
|
3688
|
-
thread_id: "thread-
|
|
3689
|
-
turn_id: "turn-
|
|
3690
|
-
|
|
5430
|
+
session_id: "triage-optout-chat-1",
|
|
5431
|
+
thread_id: "thread-optout-chat-1",
|
|
5432
|
+
turn_id: "turn-optout-chat-1",
|
|
5433
|
+
prompt: "add dark mode toggle to the settings page, but just chat about it",
|
|
3691
5434
|
},
|
|
3692
5435
|
{ cwd },
|
|
3693
5436
|
);
|
|
3694
5437
|
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
stopReason: "team_team-verify",
|
|
3701
|
-
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
3702
|
-
});
|
|
5438
|
+
const additionalContext = String(
|
|
5439
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5440
|
+
);
|
|
5441
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5442
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
3703
5443
|
|
|
3704
|
-
const
|
|
3705
|
-
|
|
3706
|
-
) as { sessions?: Record<string, unknown> };
|
|
3707
|
-
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
5444
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-chat-1", "prompt-routing-state.json");
|
|
5445
|
+
assert.equal(existsSync(stateFile), false);
|
|
3708
5446
|
} finally {
|
|
3709
5447
|
await rm(cwd, { recursive: true, force: true });
|
|
3710
5448
|
}
|
|
3711
5449
|
});
|
|
3712
5450
|
|
|
3713
|
-
it("
|
|
3714
|
-
const
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
mode: "ultraqa",
|
|
3729
|
-
phase: "diagnose",
|
|
3730
|
-
reason:
|
|
3731
|
-
"OMX ultraqa is still active (phase: diagnose); continue the task and gather fresh verification evidence before stopping.",
|
|
3732
|
-
},
|
|
3733
|
-
] as const;
|
|
3734
|
-
|
|
3735
|
-
for (const testCase of cases) {
|
|
3736
|
-
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-${testCase.mode}-repeat-`));
|
|
3737
|
-
try {
|
|
3738
|
-
const stateDir = join(cwd, ".omx", "state");
|
|
3739
|
-
await mkdir(stateDir, { recursive: true });
|
|
3740
|
-
await writeJson(join(stateDir, `${testCase.mode}-state.json`), {
|
|
3741
|
-
active: true,
|
|
3742
|
-
current_phase: testCase.phase,
|
|
3743
|
-
});
|
|
3744
|
-
|
|
3745
|
-
await dispatchCodexNativeHook(
|
|
3746
|
-
{
|
|
3747
|
-
hook_event_name: "Stop",
|
|
3748
|
-
cwd,
|
|
3749
|
-
session_id: `sess-stop-${testCase.mode}-repeat`,
|
|
3750
|
-
thread_id: `thread-stop-${testCase.mode}-repeat`,
|
|
3751
|
-
turn_id: `turn-stop-${testCase.mode}-repeat-1`,
|
|
3752
|
-
},
|
|
3753
|
-
{ cwd },
|
|
3754
|
-
);
|
|
5451
|
+
it("produces no triage advisory when prompt contains 'no workflow' opt-out", async () => {
|
|
5452
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-noworkflow-"));
|
|
5453
|
+
try {
|
|
5454
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5455
|
+
const result = await dispatchCodexNativeHook(
|
|
5456
|
+
{
|
|
5457
|
+
hook_event_name: "UserPromptSubmit",
|
|
5458
|
+
cwd,
|
|
5459
|
+
session_id: "triage-optout-noworkflow-1",
|
|
5460
|
+
thread_id: "thread-optout-noworkflow-1",
|
|
5461
|
+
turn_id: "turn-optout-noworkflow-1",
|
|
5462
|
+
prompt: "make the button blue, no workflow",
|
|
5463
|
+
},
|
|
5464
|
+
{ cwd },
|
|
5465
|
+
);
|
|
3755
5466
|
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
session_id: `sess-stop-${testCase.mode}-repeat`,
|
|
3761
|
-
thread_id: `thread-stop-${testCase.mode}-repeat`,
|
|
3762
|
-
turn_id: `turn-stop-${testCase.mode}-repeat-1`,
|
|
3763
|
-
stop_hook_active: true,
|
|
3764
|
-
},
|
|
3765
|
-
{ cwd },
|
|
3766
|
-
);
|
|
5467
|
+
const additionalContext = String(
|
|
5468
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5469
|
+
);
|
|
5470
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
3767
5471
|
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
stopReason: `${testCase.mode}_${testCase.phase}`,
|
|
3773
|
-
systemMessage: `OMX ${testCase.mode} is still active (phase: ${testCase.phase}).`,
|
|
3774
|
-
});
|
|
3775
|
-
} finally {
|
|
3776
|
-
await rm(cwd, { recursive: true, force: true });
|
|
3777
|
-
}
|
|
5472
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-noworkflow-1", "prompt-routing-state.json");
|
|
5473
|
+
assert.equal(existsSync(stateFile), false);
|
|
5474
|
+
} finally {
|
|
5475
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3778
5476
|
}
|
|
3779
5477
|
});
|
|
3780
5478
|
|
|
3781
|
-
|
|
3782
|
-
|
|
5479
|
+
// ── Group 10: Keyword on follow-up turn wins cleanly ─────────────────────
|
|
5480
|
+
|
|
5481
|
+
it("keyword on turn 2 suppresses triage and writes no triage state", async () => {
|
|
5482
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-kw-followup-"));
|
|
5483
|
+
const sessionId = "triage-kw-followup-1";
|
|
3783
5484
|
try {
|
|
3784
|
-
|
|
3785
|
-
await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
|
|
3786
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
|
|
3787
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
|
|
3788
|
-
active: true,
|
|
3789
|
-
skill: "ralplan",
|
|
3790
|
-
phase: "planning",
|
|
3791
|
-
});
|
|
3792
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
|
|
3793
|
-
active: true,
|
|
3794
|
-
current_phase: "planning",
|
|
3795
|
-
});
|
|
5485
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
3796
5486
|
|
|
5487
|
+
// Turn 1: neutral prompt — triage may or may not fire, doesn't matter
|
|
3797
5488
|
await dispatchCodexNativeHook(
|
|
3798
5489
|
{
|
|
3799
|
-
hook_event_name: "
|
|
5490
|
+
hook_event_name: "UserPromptSubmit",
|
|
3800
5491
|
cwd,
|
|
3801
|
-
session_id:
|
|
3802
|
-
thread_id: "thread-
|
|
3803
|
-
turn_id: "turn-
|
|
5492
|
+
session_id: sessionId,
|
|
5493
|
+
thread_id: "thread-kw-followup-1",
|
|
5494
|
+
turn_id: "turn-kw-followup-1",
|
|
5495
|
+
prompt: "hello",
|
|
3804
5496
|
},
|
|
3805
5497
|
{ cwd },
|
|
3806
5498
|
);
|
|
3807
5499
|
|
|
3808
|
-
|
|
5500
|
+
// Turn 2: keyword prompt — keyword fast-path runs, triage does NOT add extra advisory
|
|
5501
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
3809
5502
|
{
|
|
3810
|
-
hook_event_name: "
|
|
5503
|
+
hook_event_name: "UserPromptSubmit",
|
|
3811
5504
|
cwd,
|
|
3812
|
-
session_id:
|
|
3813
|
-
thread_id: "thread-
|
|
3814
|
-
turn_id: "turn-
|
|
3815
|
-
|
|
5505
|
+
session_id: sessionId,
|
|
5506
|
+
thread_id: "thread-kw-followup-1",
|
|
5507
|
+
turn_id: "turn-kw-followup-2",
|
|
5508
|
+
prompt: "$ralph continue",
|
|
3816
5509
|
},
|
|
3817
5510
|
{ cwd },
|
|
3818
5511
|
);
|
|
3819
5512
|
|
|
3820
|
-
assert.equal(
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
5513
|
+
assert.equal(turn2.skillState?.skill, "ralph");
|
|
5514
|
+
|
|
5515
|
+
const ctx2 = String(
|
|
5516
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5517
|
+
);
|
|
5518
|
+
assert.doesNotMatch(ctx2, /multi-step goal with no workflow keyword/);
|
|
5519
|
+
assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
|
|
5520
|
+
assert.doesNotMatch(ctx2, /narrow edit-shaped/);
|
|
5521
|
+
assert.doesNotMatch(ctx2, /visual\/style request/);
|
|
5522
|
+
|
|
5523
|
+
// No triage state written on the keyword turn
|
|
5524
|
+
const triageState = join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json");
|
|
5525
|
+
// The state from turn 1 (if any) must not have been created either (hello = PASS)
|
|
5526
|
+
assert.equal(existsSync(triageState), false);
|
|
3828
5527
|
} finally {
|
|
3829
5528
|
await rm(cwd, { recursive: true, force: true });
|
|
3830
5529
|
}
|
|
3831
5530
|
});
|
|
3832
5531
|
|
|
3833
|
-
|
|
3834
|
-
|
|
5532
|
+
// ── Group 11: Config-disabled path ───────────────────────────────────────
|
|
5533
|
+
|
|
5534
|
+
it("produces no triage advisory and no state when triage is disabled in config", async () => {
|
|
5535
|
+
const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-home-"));
|
|
5536
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-cwd-"));
|
|
3835
5537
|
try {
|
|
3836
|
-
|
|
3837
|
-
await
|
|
3838
|
-
|
|
3839
|
-
await writeJson(join(stateDir, "team-state.json"), {
|
|
3840
|
-
active: true,
|
|
3841
|
-
current_phase: "starting",
|
|
3842
|
-
team_name: "stale-root-team",
|
|
3843
|
-
session_id: "sess-other",
|
|
3844
|
-
});
|
|
3845
|
-
await writeJson(join(stateDir, "team", "stale-root-team", "phase.json"), {
|
|
3846
|
-
current_phase: "team-exec",
|
|
3847
|
-
max_fix_attempts: 3,
|
|
3848
|
-
current_fix_attempt: 0,
|
|
3849
|
-
transitions: [],
|
|
3850
|
-
updated_at: new Date().toISOString(),
|
|
5538
|
+
// Write a .omx-config.json in the fake CODEX_HOME that disables triage
|
|
5539
|
+
await writeJson(join(tmpHome, ".omx-config.json"), {
|
|
5540
|
+
promptRouting: { triage: { enabled: false } },
|
|
3851
5541
|
});
|
|
5542
|
+
process.env.CODEX_HOME = tmpHome;
|
|
5543
|
+
resetTriageConfigCache();
|
|
3852
5544
|
|
|
5545
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
3853
5546
|
const result = await dispatchCodexNativeHook(
|
|
3854
5547
|
{
|
|
3855
|
-
hook_event_name: "
|
|
5548
|
+
hook_event_name: "UserPromptSubmit",
|
|
3856
5549
|
cwd,
|
|
3857
|
-
session_id: "
|
|
5550
|
+
session_id: "triage-disabled-1",
|
|
5551
|
+
thread_id: "thread-triage-disabled-1",
|
|
5552
|
+
turn_id: "turn-triage-disabled-1",
|
|
5553
|
+
prompt: "add dark mode toggle to the settings page",
|
|
3858
5554
|
},
|
|
3859
5555
|
{ cwd },
|
|
3860
5556
|
);
|
|
3861
5557
|
|
|
3862
|
-
|
|
3863
|
-
|
|
5558
|
+
const additionalContext = String(
|
|
5559
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5560
|
+
);
|
|
5561
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5562
|
+
|
|
5563
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-disabled-1", "prompt-routing-state.json");
|
|
5564
|
+
assert.equal(existsSync(stateFile), false);
|
|
3864
5565
|
} finally {
|
|
5566
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
3865
5567
|
await rm(cwd, { recursive: true, force: true });
|
|
3866
5568
|
}
|
|
3867
5569
|
});
|
|
3868
5570
|
|
|
3869
|
-
it("
|
|
3870
|
-
const
|
|
5571
|
+
it("keeps triage default-enabled when config omits promptRouting.triage.enabled", async () => {
|
|
5572
|
+
const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-home-"));
|
|
5573
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-cwd-"));
|
|
5574
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
3871
5575
|
try {
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
3875
|
-
await writeJson(join(stateDir, "team-state.json"), {
|
|
3876
|
-
active: true,
|
|
3877
|
-
current_phase: "starting",
|
|
3878
|
-
team_name: "cleaned-team",
|
|
3879
|
-
session_id: "sess-current",
|
|
5576
|
+
await writeJson(join(tmpHome, ".omx-config.json"), {
|
|
5577
|
+
promptRouting: {},
|
|
3880
5578
|
});
|
|
5579
|
+
process.env.CODEX_HOME = tmpHome;
|
|
5580
|
+
resetTriageConfigCache();
|
|
3881
5581
|
|
|
5582
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
3882
5583
|
const result = await dispatchCodexNativeHook(
|
|
3883
5584
|
{
|
|
3884
|
-
hook_event_name: "
|
|
5585
|
+
hook_event_name: "UserPromptSubmit",
|
|
3885
5586
|
cwd,
|
|
3886
|
-
session_id: "
|
|
5587
|
+
session_id: "triage-defaulted-1",
|
|
5588
|
+
thread_id: "thread-triage-defaulted-1",
|
|
5589
|
+
turn_id: "turn-triage-defaulted-1",
|
|
5590
|
+
prompt: "add dark mode toggle to the settings page",
|
|
3887
5591
|
},
|
|
3888
5592
|
{ cwd },
|
|
3889
5593
|
);
|
|
3890
5594
|
|
|
3891
|
-
|
|
3892
|
-
|
|
5595
|
+
const additionalContext = String(
|
|
5596
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5597
|
+
);
|
|
5598
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5599
|
+
|
|
5600
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-defaulted-1", "prompt-routing-state.json");
|
|
5601
|
+
assert.equal(existsSync(stateFile), true);
|
|
3893
5602
|
} finally {
|
|
5603
|
+
if (typeof previousCodexHome === "string") process.env.CODEX_HOME = previousCodexHome;
|
|
5604
|
+
else delete process.env.CODEX_HOME;
|
|
5605
|
+
resetTriageConfigCache();
|
|
5606
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
3894
5607
|
await rm(cwd, { recursive: true, force: true });
|
|
3895
5608
|
}
|
|
3896
5609
|
});
|
|
3897
5610
|
|
|
3898
|
-
it("
|
|
3899
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-
|
|
5611
|
+
it("does not suppress a short anchored follow-up that is a new request", async () => {
|
|
5612
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-short-new-request-"));
|
|
5613
|
+
const sessionId = "triage-short-new-request-1";
|
|
3900
5614
|
try {
|
|
3901
|
-
|
|
3902
|
-
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3903
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
3904
|
-
await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
|
|
3905
|
-
active: true,
|
|
3906
|
-
current_phase: "starting",
|
|
3907
|
-
team_name: "current-team",
|
|
3908
|
-
session_id: "sess-current",
|
|
3909
|
-
});
|
|
3910
|
-
await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
|
|
3911
|
-
current_phase: "team-verify",
|
|
3912
|
-
max_fix_attempts: 3,
|
|
3913
|
-
current_fix_attempt: 1,
|
|
3914
|
-
transitions: [],
|
|
3915
|
-
updated_at: new Date().toISOString(),
|
|
3916
|
-
});
|
|
3917
|
-
await writeJson(join(stateDir, "team-state.json"), {
|
|
3918
|
-
active: true,
|
|
3919
|
-
current_phase: "starting",
|
|
3920
|
-
team_name: "stale-root-team",
|
|
3921
|
-
session_id: "sess-other",
|
|
3922
|
-
});
|
|
3923
|
-
await writeJson(join(stateDir, "team", "stale-root-team", "phase.json"), {
|
|
3924
|
-
current_phase: "team-exec",
|
|
3925
|
-
max_fix_attempts: 3,
|
|
3926
|
-
current_fix_attempt: 0,
|
|
3927
|
-
transitions: [],
|
|
3928
|
-
updated_at: new Date().toISOString(),
|
|
3929
|
-
});
|
|
5615
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
3930
5616
|
|
|
3931
|
-
|
|
5617
|
+
await dispatchCodexNativeHook(
|
|
3932
5618
|
{
|
|
3933
|
-
hook_event_name: "
|
|
5619
|
+
hook_event_name: "UserPromptSubmit",
|
|
3934
5620
|
cwd,
|
|
3935
|
-
session_id:
|
|
5621
|
+
session_id: sessionId,
|
|
5622
|
+
thread_id: "thread-short-new-request-1",
|
|
5623
|
+
turn_id: "turn-short-new-request-1",
|
|
5624
|
+
prompt: "add dark mode toggle to the settings page",
|
|
3936
5625
|
},
|
|
3937
5626
|
{ cwd },
|
|
3938
5627
|
);
|
|
3939
5628
|
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
5629
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
5630
|
+
{
|
|
5631
|
+
hook_event_name: "UserPromptSubmit",
|
|
5632
|
+
cwd,
|
|
5633
|
+
session_id: sessionId,
|
|
5634
|
+
thread_id: "thread-short-new-request-1",
|
|
5635
|
+
turn_id: "turn-short-new-request-2",
|
|
5636
|
+
prompt: "fix typo in src/foo.ts",
|
|
5637
|
+
},
|
|
5638
|
+
{ cwd },
|
|
5639
|
+
);
|
|
5640
|
+
|
|
5641
|
+
const ctx2 = String(
|
|
5642
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5643
|
+
);
|
|
5644
|
+
assert.match(ctx2, /narrow edit-shaped/);
|
|
3948
5645
|
} finally {
|
|
3949
5646
|
await rm(cwd, { recursive: true, force: true });
|
|
3950
5647
|
}
|
|
3951
5648
|
});
|
|
3952
5649
|
|
|
3953
|
-
it("
|
|
3954
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-
|
|
5650
|
+
it("skips triage state persistence for malformed explicit session ids without writing root state", async () => {
|
|
5651
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-invalid-session-"));
|
|
3955
5652
|
try {
|
|
3956
|
-
|
|
3957
|
-
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3958
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
3959
|
-
await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
|
|
3960
|
-
active: false,
|
|
3961
|
-
current_phase: "complete",
|
|
3962
|
-
team_name: "scoped-finished-team",
|
|
3963
|
-
session_id: "sess-current",
|
|
3964
|
-
});
|
|
3965
|
-
await writeJson(join(stateDir, "team-state.json"), {
|
|
3966
|
-
active: true,
|
|
3967
|
-
current_phase: "starting",
|
|
3968
|
-
team_name: "root-fallback-team",
|
|
3969
|
-
session_id: "sess-current",
|
|
3970
|
-
});
|
|
3971
|
-
await writeJson(join(stateDir, "team", "root-fallback-team", "phase.json"), {
|
|
3972
|
-
current_phase: "team-exec",
|
|
3973
|
-
max_fix_attempts: 3,
|
|
3974
|
-
current_fix_attempt: 0,
|
|
3975
|
-
transitions: [],
|
|
3976
|
-
updated_at: new Date().toISOString(),
|
|
3977
|
-
});
|
|
3978
|
-
|
|
5653
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
3979
5654
|
const result = await dispatchCodexNativeHook(
|
|
3980
5655
|
{
|
|
3981
|
-
hook_event_name: "
|
|
5656
|
+
hook_event_name: "UserPromptSubmit",
|
|
3982
5657
|
cwd,
|
|
3983
|
-
session_id: "
|
|
5658
|
+
session_id: "bad/session",
|
|
5659
|
+
thread_id: "thread-triage-invalid-session-1",
|
|
5660
|
+
turn_id: "turn-triage-invalid-session-1",
|
|
5661
|
+
prompt: "add dark mode toggle to the settings page",
|
|
3984
5662
|
},
|
|
3985
5663
|
{ cwd },
|
|
3986
5664
|
);
|
|
3987
5665
|
|
|
3988
|
-
|
|
3989
|
-
|
|
5666
|
+
const additionalContext = String(
|
|
5667
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5668
|
+
);
|
|
5669
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5670
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "prompt-routing-state.json")), false);
|
|
3990
5671
|
} finally {
|
|
3991
5672
|
await rm(cwd, { recursive: true, force: true });
|
|
3992
5673
|
}
|