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,15 +4,36 @@ 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 { initTeamState, readTeamLeaderAttention, readTeamPhase, writeTeamLeaderAttention, } from "../../team/state.js";
|
|
10
|
-
import { dispatchCodexNativeHook, mapCodexHookEventToOmxEvent, resolveSessionOwnerPidFromAncestry, } from "../codex-native-hook.js";
|
|
11
|
+
import { dispatchCodexNativeHook, isCodexNativeHookMainModule, mapCodexHookEventToOmxEvent, resolveSessionOwnerPidFromAncestry, } from "../codex-native-hook.js";
|
|
11
12
|
import { writeSessionStart } from "../../hooks/session.js";
|
|
13
|
+
import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
12
14
|
async function writeJson(path, value) {
|
|
13
15
|
await mkdir(dirname(path), { recursive: true }).catch(() => { });
|
|
14
16
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
15
17
|
}
|
|
18
|
+
async function writeHookCounterPlugin(cwd) {
|
|
19
|
+
const markerPath = join(cwd, ".omx", "stop-hook-counter.json");
|
|
20
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
21
|
+
await writeFile(join(cwd, ".omx", "hooks", "count-stop-hook.mjs"), `import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
|
|
24
|
+
export async function onHookEvent(event) {
|
|
25
|
+
if (event.event !== "stop") return;
|
|
26
|
+
const outPath = join(process.cwd(), ".omx", "stop-hook-counter.json");
|
|
27
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
28
|
+
let count = 0;
|
|
29
|
+
try {
|
|
30
|
+
count = JSON.parse(await readFile(outPath, "utf-8")).count || 0;
|
|
31
|
+
} catch {}
|
|
32
|
+
await writeFile(outPath, JSON.stringify({ count: count + 1 }, null, 2));
|
|
33
|
+
}
|
|
34
|
+
`, "utf-8");
|
|
35
|
+
return markerPath;
|
|
36
|
+
}
|
|
16
37
|
async function writeReleaseReadinessLeaderAttention(teamName, sessionId, cwd, options) {
|
|
17
38
|
await writeTeamLeaderAttention(teamName, {
|
|
18
39
|
team_name: teamName,
|
|
@@ -90,6 +111,13 @@ describe("codex native hook config", () => {
|
|
|
90
111
|
});
|
|
91
112
|
});
|
|
92
113
|
describe("codex native hook dispatch", () => {
|
|
114
|
+
it("treats space-containing argv entry paths as the main module", () => {
|
|
115
|
+
const entryPath = "/tmp/omx native/codex-native-hook.js";
|
|
116
|
+
assert.equal(isCodexNativeHookMainModule(pathToFileURL(entryPath).href, entryPath), true);
|
|
117
|
+
});
|
|
118
|
+
it("does not treat a different module url as the main module", () => {
|
|
119
|
+
assert.equal(isCodexNativeHookMainModule(pathToFileURL("/tmp/omx native/other-script.js").href, "/tmp/omx native/codex-native-hook.js"), false);
|
|
120
|
+
});
|
|
93
121
|
it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
|
|
94
122
|
const stdout = execFileSync(process.execPath, [join(process.cwd(), "dist", "scripts", "codex-native-hook.js")], {
|
|
95
123
|
cwd: process.cwd(),
|
|
@@ -174,7 +202,41 @@ describe("codex native hook dispatch", () => {
|
|
|
174
202
|
await rm(cwd, { recursive: true, force: true });
|
|
175
203
|
}
|
|
176
204
|
});
|
|
177
|
-
it("
|
|
205
|
+
it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
|
|
206
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
|
|
207
|
+
try {
|
|
208
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
209
|
+
const canonicalSessionId = "omx-launch-hud";
|
|
210
|
+
const nativeSessionId = "codex-native-hud";
|
|
211
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
212
|
+
await writeSessionStart(cwd, canonicalSessionId);
|
|
213
|
+
let reconcileCall = null;
|
|
214
|
+
const promptResult = await dispatchCodexNativeHook({
|
|
215
|
+
hook_event_name: "UserPromptSubmit",
|
|
216
|
+
cwd,
|
|
217
|
+
session_id: nativeSessionId,
|
|
218
|
+
thread_id: "thread-hud",
|
|
219
|
+
turn_id: "turn-hud",
|
|
220
|
+
prompt: "$ralplan fix orphaned hud session handoff",
|
|
221
|
+
}, {
|
|
222
|
+
cwd,
|
|
223
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
224
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
|
|
225
|
+
return { status: 'recreated', paneId: '%9', desiredHeight: 3, duplicateCount: 0 };
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
229
|
+
assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
|
|
230
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), true);
|
|
231
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), true);
|
|
232
|
+
assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json")), false);
|
|
233
|
+
assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "ralplan-state.json")), false);
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
await rm(cwd, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
it("adds .omx/ to git info/exclude during SessionStart instead of mutating repo .gitignore", async () => {
|
|
178
240
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-gitignore-"));
|
|
179
241
|
try {
|
|
180
242
|
await writeFile(join(cwd, ".gitignore"), "node_modules/\n");
|
|
@@ -186,8 +248,55 @@ describe("codex native hook dispatch", () => {
|
|
|
186
248
|
}, { cwd, sessionOwnerPid: 43210 });
|
|
187
249
|
assert.equal(result.omxEventName, "session-start");
|
|
188
250
|
const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
|
|
189
|
-
assert.
|
|
190
|
-
|
|
251
|
+
assert.equal(gitignore, "node_modules/\n");
|
|
252
|
+
const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
|
|
253
|
+
assert.match(exclude, /(?:^|\n)\.omx\/\n/);
|
|
254
|
+
assert.match(JSON.stringify(result.outputJson), /Added \.omx\/ to .*\.git[\/]info[\/]exclude/);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
await rm(cwd, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
it("keeps SessionStart quiet when .omx/ is already ignored by repo-level gitignore", async () => {
|
|
261
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-existing-ignore-"));
|
|
262
|
+
try {
|
|
263
|
+
await writeFile(join(cwd, ".gitignore"), "node_modules/\n.omx/\n");
|
|
264
|
+
execFileSync("git", ["init"], { cwd, stdio: "pipe" });
|
|
265
|
+
const result = await dispatchCodexNativeHook({
|
|
266
|
+
hook_event_name: "SessionStart",
|
|
267
|
+
cwd,
|
|
268
|
+
session_id: "sess-gitignore-existing",
|
|
269
|
+
}, { cwd, sessionOwnerPid: 43210 });
|
|
270
|
+
assert.equal(result.omxEventName, "session-start");
|
|
271
|
+
const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
|
|
272
|
+
assert.equal(gitignore, "node_modules/\n.omx/\n");
|
|
273
|
+
const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
|
|
274
|
+
assert.doesNotMatch(exclude, /(?:^|\n)\.omx\/\n/);
|
|
275
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /Added \.omx\//);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
await rm(cwd, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
it("respects existing Git ignore resolution before writing local excludes", async () => {
|
|
282
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-global-ignore-"));
|
|
283
|
+
const excludesFile = join(cwd, "global-ignore");
|
|
284
|
+
try {
|
|
285
|
+
await writeFile(join(cwd, ".gitignore"), "node_modules/\n");
|
|
286
|
+
await writeFile(excludesFile, ".omx/\n");
|
|
287
|
+
execFileSync("git", ["init"], { cwd, stdio: "pipe" });
|
|
288
|
+
execFileSync("git", ["config", "core.excludesfile", excludesFile], { cwd, stdio: "pipe" });
|
|
289
|
+
const result = await dispatchCodexNativeHook({
|
|
290
|
+
hook_event_name: "SessionStart",
|
|
291
|
+
cwd,
|
|
292
|
+
session_id: "sess-gitignore-global",
|
|
293
|
+
}, { cwd, sessionOwnerPid: 43210 });
|
|
294
|
+
assert.equal(result.omxEventName, "session-start");
|
|
295
|
+
const gitignore = await readFile(join(cwd, ".gitignore"), "utf-8");
|
|
296
|
+
assert.equal(gitignore, "node_modules/\n");
|
|
297
|
+
const exclude = await readFile(join(cwd, ".git", "info", "exclude"), "utf-8");
|
|
298
|
+
assert.doesNotMatch(exclude, /(?:^|\n)\.omx\/\n/);
|
|
299
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /Added \.omx\//);
|
|
191
300
|
}
|
|
192
301
|
finally {
|
|
193
302
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -352,7 +461,12 @@ describe("codex native hook dispatch", () => {
|
|
|
352
461
|
}, { cwd });
|
|
353
462
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
354
463
|
assert.equal(result.skillState, null);
|
|
355
|
-
|
|
464
|
+
// Triage may inject advisory LIGHT/explore context for the question-shaped
|
|
465
|
+
// prompt, but the invariant this test guards is that no Ralph workflow state
|
|
466
|
+
// is seeded and no Ralph-activation message is emitted.
|
|
467
|
+
const advisoryContext = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
468
|
+
assert.doesNotMatch(advisoryContext, /skill:\s*ralph/i);
|
|
469
|
+
assert.doesNotMatch(advisoryContext, /ralph-state\.json/i);
|
|
356
470
|
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
357
471
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "skill-active-state.json")), false);
|
|
358
472
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "ralph-state.json")), false);
|
|
@@ -361,6 +475,54 @@ describe("codex native hook dispatch", () => {
|
|
|
361
475
|
await rm(cwd, { recursive: true, force: true });
|
|
362
476
|
}
|
|
363
477
|
});
|
|
478
|
+
it("adds execution handoff context for non-keyword prompts that authorize implementation", async () => {
|
|
479
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-execution-handoff-"));
|
|
480
|
+
try {
|
|
481
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
482
|
+
const prompts = [
|
|
483
|
+
"按照这个plan开始执行优化",
|
|
484
|
+
"开始执行",
|
|
485
|
+
"继续优化",
|
|
486
|
+
"直接修复",
|
|
487
|
+
];
|
|
488
|
+
for (const [index, prompt] of prompts.entries()) {
|
|
489
|
+
const result = await dispatchCodexNativeHook({
|
|
490
|
+
hook_event_name: "UserPromptSubmit",
|
|
491
|
+
cwd,
|
|
492
|
+
session_id: `sess-exec-handoff-${index}`,
|
|
493
|
+
thread_id: `thread-exec-handoff-${index}`,
|
|
494
|
+
turn_id: `turn-exec-handoff-${index}`,
|
|
495
|
+
prompt,
|
|
496
|
+
}, { cwd });
|
|
497
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
498
|
+
assert.match(message, /execution handoff/i, prompt);
|
|
499
|
+
assert.match(message, /Do not restate the prior plan/i, prompt);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
await rm(cwd, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
it("adds latest-followup priority context for short same-thread follow-up prompts", async () => {
|
|
507
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-followup-priority-"));
|
|
508
|
+
try {
|
|
509
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
510
|
+
const result = await dispatchCodexNativeHook({
|
|
511
|
+
hook_event_name: "UserPromptSubmit",
|
|
512
|
+
cwd,
|
|
513
|
+
session_id: "sess-followup-priority",
|
|
514
|
+
thread_id: "thread-followup-priority",
|
|
515
|
+
turn_id: "turn-followup-priority",
|
|
516
|
+
prompt: "这些优化都做了么",
|
|
517
|
+
}, { cwd });
|
|
518
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
519
|
+
assert.match(message, /same-thread follow-up/i);
|
|
520
|
+
assert.match(message, /prefer it over older unresolved prompts/i);
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
await rm(cwd, { recursive: true, force: true });
|
|
524
|
+
}
|
|
525
|
+
});
|
|
364
526
|
it("clarifies that prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
|
|
365
527
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-routing-"));
|
|
366
528
|
try {
|
|
@@ -385,6 +547,125 @@ describe("codex native hook dispatch", () => {
|
|
|
385
547
|
await rm(cwd, { recursive: true, force: true });
|
|
386
548
|
}
|
|
387
549
|
});
|
|
550
|
+
it("keeps bare keep-going continuation on the active autopilot skill instead of denying with generic ralph overlap", async () => {
|
|
551
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-bare-continuation-"));
|
|
552
|
+
try {
|
|
553
|
+
const sessionId = "sess-autopilot-cont";
|
|
554
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
555
|
+
await mkdir(sessionDir, { recursive: true });
|
|
556
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
557
|
+
version: 1,
|
|
558
|
+
active: true,
|
|
559
|
+
skill: "autopilot",
|
|
560
|
+
keyword: "$autopilot",
|
|
561
|
+
phase: "planning",
|
|
562
|
+
session_id: sessionId,
|
|
563
|
+
active_skills: [
|
|
564
|
+
{ skill: "autopilot", phase: "planning", active: true, session_id: sessionId },
|
|
565
|
+
],
|
|
566
|
+
});
|
|
567
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
568
|
+
active: true,
|
|
569
|
+
mode: "autopilot",
|
|
570
|
+
current_phase: "execution",
|
|
571
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
572
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
573
|
+
session_id: sessionId,
|
|
574
|
+
});
|
|
575
|
+
const result = await dispatchCodexNativeHook({
|
|
576
|
+
hook_event_name: "UserPromptSubmit",
|
|
577
|
+
cwd,
|
|
578
|
+
session_id: sessionId,
|
|
579
|
+
thread_id: "thread-autopilot-cont",
|
|
580
|
+
turn_id: "turn-autopilot-cont",
|
|
581
|
+
prompt: "\ keep going now",
|
|
582
|
+
}, { cwd });
|
|
583
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
584
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
585
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
586
|
+
assert.match(message, /"keep going" -> ralph/);
|
|
587
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
588
|
+
assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
|
|
589
|
+
assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
|
|
590
|
+
assert.equal(existsSync(join(sessionDir, "ralph-state.json")), false);
|
|
591
|
+
}
|
|
592
|
+
finally {
|
|
593
|
+
await rm(cwd, { recursive: true, force: true });
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
it("clarifies that prompt-side deep-interview activation must use omx question", async () => {
|
|
597
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
|
|
598
|
+
try {
|
|
599
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
600
|
+
const result = await dispatchCodexNativeHook({
|
|
601
|
+
hook_event_name: "UserPromptSubmit",
|
|
602
|
+
cwd,
|
|
603
|
+
session_id: "sess-deep-interview-msg",
|
|
604
|
+
thread_id: "thread-deep-interview-msg",
|
|
605
|
+
turn_id: "turn-deep-interview-msg",
|
|
606
|
+
prompt: "$deep-interview gather requirements",
|
|
607
|
+
}, { cwd });
|
|
608
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
609
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
610
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
611
|
+
assert.match(message, /\$deep-interview" -> deep-interview/);
|
|
612
|
+
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\./);
|
|
613
|
+
assert.match(message, /Deep-interview must ask each interview round via `omx question`/);
|
|
614
|
+
assert.match(message, /do not fall back to `request_user_input` or plain-text questioning/i);
|
|
615
|
+
assert.match(message, /If bare `omx question` is unavailable in this reused session, use the current-session CLI bridge command:/);
|
|
616
|
+
assert.match(message, /`'.+' '.+dist\/cli\/omx\.js' question`/);
|
|
617
|
+
assert.match(message, /Stop remains blocked while a deep-interview question obligation is pending\./);
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
await rm(cwd, { recursive: true, force: true });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
it("keeps bare keep-going continuation on the active ralph skill without resetting through generic keep-going routing", async () => {
|
|
624
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-bare-continuation-"));
|
|
625
|
+
try {
|
|
626
|
+
const sessionId = "sess-ralph-cont";
|
|
627
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
628
|
+
await mkdir(sessionDir, { recursive: true });
|
|
629
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
630
|
+
version: 1,
|
|
631
|
+
active: true,
|
|
632
|
+
skill: "ralph",
|
|
633
|
+
keyword: "$ralph",
|
|
634
|
+
phase: "executing",
|
|
635
|
+
session_id: sessionId,
|
|
636
|
+
active_skills: [
|
|
637
|
+
{ skill: "ralph", phase: "executing", active: true, session_id: sessionId },
|
|
638
|
+
],
|
|
639
|
+
});
|
|
640
|
+
await writeJson(join(sessionDir, "ralph-state.json"), {
|
|
641
|
+
active: true,
|
|
642
|
+
mode: "ralph",
|
|
643
|
+
current_phase: "verifying",
|
|
644
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
645
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
646
|
+
iteration: 4,
|
|
647
|
+
max_iterations: 50,
|
|
648
|
+
session_id: sessionId,
|
|
649
|
+
});
|
|
650
|
+
const result = await dispatchCodexNativeHook({
|
|
651
|
+
hook_event_name: "UserPromptSubmit",
|
|
652
|
+
cwd,
|
|
653
|
+
session_id: sessionId,
|
|
654
|
+
thread_id: "thread-ralph-cont",
|
|
655
|
+
turn_id: "turn-ralph-cont",
|
|
656
|
+
prompt: "keep going now",
|
|
657
|
+
}, { cwd });
|
|
658
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
659
|
+
assert.equal(result.skillState?.skill, "ralph");
|
|
660
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
661
|
+
assert.match(message, /"keep going" -> ralph/);
|
|
662
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
663
|
+
assert.doesNotMatch(message, /mode transiting:/);
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
await rm(cwd, { recursive: true, force: true });
|
|
667
|
+
}
|
|
668
|
+
});
|
|
388
669
|
it("ignores generic wrapper fields so metadata cannot trigger workflow routing or Stop blocking", async () => {
|
|
389
670
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-wrapper-metadata-"));
|
|
390
671
|
try {
|
|
@@ -1517,6 +1798,28 @@ esac
|
|
|
1517
1798
|
await rm(cwd, { recursive: true, force: true });
|
|
1518
1799
|
}
|
|
1519
1800
|
});
|
|
1801
|
+
it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
|
|
1802
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
|
|
1803
|
+
try {
|
|
1804
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1805
|
+
await mkdir(stateDir, { recursive: true });
|
|
1806
|
+
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
1807
|
+
active: true,
|
|
1808
|
+
current_phase: "execution",
|
|
1809
|
+
run_outcome: "blocked_on_user",
|
|
1810
|
+
});
|
|
1811
|
+
const result = await dispatchCodexNativeHook({
|
|
1812
|
+
hook_event_name: "Stop",
|
|
1813
|
+
cwd,
|
|
1814
|
+
session_id: "sess-stop-autopilot-blocked-outcome",
|
|
1815
|
+
}, { cwd });
|
|
1816
|
+
assert.equal(result.omxEventName, "stop");
|
|
1817
|
+
assert.equal(result.outputJson, null);
|
|
1818
|
+
}
|
|
1819
|
+
finally {
|
|
1820
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1520
1823
|
it("returns Stop continuation output while Ultrawork is active", async () => {
|
|
1521
1824
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
|
|
1522
1825
|
try {
|
|
@@ -2082,139 +2385,534 @@ esac
|
|
|
2082
2385
|
await rm(cwd, { recursive: true, force: true });
|
|
2083
2386
|
}
|
|
2084
2387
|
});
|
|
2085
|
-
it("
|
|
2086
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2388
|
+
it("blocks Stop while autoresearch is active without validator completion", async () => {
|
|
2389
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-"));
|
|
2087
2390
|
try {
|
|
2088
2391
|
const stateDir = join(cwd, ".omx", "state");
|
|
2089
|
-
await mkdir(join(stateDir, "sessions", "sess-stop-
|
|
2090
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-
|
|
2091
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-
|
|
2092
|
-
active: true,
|
|
2093
|
-
skill: "deep-interview",
|
|
2094
|
-
phase: "planning",
|
|
2095
|
-
});
|
|
2096
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
|
|
2392
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch"), { recursive: true });
|
|
2393
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch", cwd });
|
|
2394
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch", "autoresearch-state.json"), {
|
|
2097
2395
|
active: true,
|
|
2098
|
-
|
|
2396
|
+
mode: "autoresearch",
|
|
2397
|
+
current_phase: "executing",
|
|
2398
|
+
session_id: "sess-stop-autoresearch",
|
|
2399
|
+
validation_mode: "mission-validator-script",
|
|
2400
|
+
mission_validator_command: "node scripts/validate.js",
|
|
2401
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
2099
2402
|
});
|
|
2100
2403
|
const result = await dispatchCodexNativeHook({
|
|
2101
2404
|
hook_event_name: "Stop",
|
|
2102
2405
|
cwd,
|
|
2103
|
-
session_id: "sess-stop-
|
|
2406
|
+
session_id: "sess-stop-autoresearch",
|
|
2104
2407
|
}, { cwd });
|
|
2105
|
-
assert.equal(result.
|
|
2408
|
+
assert.equal(result.omxEventName, "stop");
|
|
2409
|
+
assert.deepEqual(result.outputJson, {
|
|
2410
|
+
decision: "block",
|
|
2411
|
+
reason: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
|
|
2412
|
+
stopReason: "autoresearch_executing",
|
|
2413
|
+
systemMessage: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
|
|
2414
|
+
});
|
|
2106
2415
|
}
|
|
2107
2416
|
finally {
|
|
2108
2417
|
await rm(cwd, { recursive: true, force: true });
|
|
2109
2418
|
}
|
|
2110
2419
|
});
|
|
2111
|
-
it("
|
|
2112
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2420
|
+
it("allows Stop once autoresearch validator evidence is complete", async () => {
|
|
2421
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-complete-"));
|
|
2113
2422
|
try {
|
|
2114
2423
|
const stateDir = join(cwd, ".omx", "state");
|
|
2115
|
-
|
|
2116
|
-
await
|
|
2424
|
+
const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
|
|
2425
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch-complete"), { recursive: true });
|
|
2426
|
+
await mkdir(specDir, { recursive: true });
|
|
2427
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch-complete", cwd });
|
|
2428
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch-complete", "autoresearch-state.json"), {
|
|
2117
2429
|
active: true,
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
session_id: "",
|
|
2121
|
-
|
|
2430
|
+
mode: "autoresearch",
|
|
2431
|
+
current_phase: "reviewing",
|
|
2432
|
+
session_id: "sess-stop-autoresearch-complete",
|
|
2433
|
+
validation_mode: "mission-validator-script",
|
|
2434
|
+
mission_validator_command: "node scripts/validate.js",
|
|
2435
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
2122
2436
|
});
|
|
2437
|
+
await writeJson(join(specDir, 'completion.json'), { status: 'passed', passed: true });
|
|
2123
2438
|
const result = await dispatchCodexNativeHook({
|
|
2124
2439
|
hook_event_name: "Stop",
|
|
2125
2440
|
cwd,
|
|
2126
|
-
session_id: "sess-stop-
|
|
2127
|
-
thread_id: "main-thread",
|
|
2441
|
+
session_id: "sess-stop-autoresearch-complete",
|
|
2128
2442
|
}, { cwd });
|
|
2443
|
+
assert.equal(result.omxEventName, "stop");
|
|
2129
2444
|
assert.equal(result.outputJson, null);
|
|
2130
2445
|
}
|
|
2131
2446
|
finally {
|
|
2132
2447
|
await rm(cwd, { recursive: true, force: true });
|
|
2133
2448
|
}
|
|
2134
2449
|
});
|
|
2135
|
-
it("
|
|
2136
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
|
|
2450
|
+
it("does not block Stop from stale root autoresearch state when the explicit session has no scoped autoresearch state", async () => {
|
|
2451
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autoresearch-"));
|
|
2137
2452
|
try {
|
|
2138
2453
|
const stateDir = join(cwd, ".omx", "state");
|
|
2139
|
-
|
|
2140
|
-
await
|
|
2454
|
+
const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
|
|
2455
|
+
await mkdir(join(stateDir, 'sessions', 'sess-current'), { recursive: true });
|
|
2456
|
+
await mkdir(specDir, { recursive: true });
|
|
2457
|
+
await writeJson(join(stateDir, 'session.json'), { session_id: 'sess-current', cwd });
|
|
2458
|
+
await writeJson(join(stateDir, 'autoresearch-state.json'), {
|
|
2141
2459
|
active: true,
|
|
2142
|
-
|
|
2143
|
-
|
|
2460
|
+
mode: 'autoresearch',
|
|
2461
|
+
current_phase: 'executing',
|
|
2462
|
+
validation_mode: 'mission-validator-script',
|
|
2463
|
+
mission_validator_command: 'node scripts/validate.js',
|
|
2464
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
2465
|
+
});
|
|
2144
2466
|
const result = await dispatchCodexNativeHook({
|
|
2145
|
-
hook_event_name:
|
|
2467
|
+
hook_event_name: 'Stop',
|
|
2146
2468
|
cwd,
|
|
2469
|
+
session_id: 'sess-current',
|
|
2147
2470
|
}, { cwd });
|
|
2148
|
-
assert.equal(result.omxEventName,
|
|
2149
|
-
assert.
|
|
2150
|
-
decision: "block",
|
|
2151
|
-
reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2152
|
-
stopReason: "ralph_executing",
|
|
2153
|
-
systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2154
|
-
});
|
|
2471
|
+
assert.equal(result.omxEventName, 'stop');
|
|
2472
|
+
assert.equal(result.outputJson, null);
|
|
2155
2473
|
}
|
|
2156
2474
|
finally {
|
|
2157
2475
|
await rm(cwd, { recursive: true, force: true });
|
|
2158
2476
|
}
|
|
2159
2477
|
});
|
|
2160
|
-
it("
|
|
2161
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2478
|
+
it("does not block Stop solely because deep-interview is active", async () => {
|
|
2479
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
|
|
2162
2480
|
try {
|
|
2163
2481
|
const stateDir = join(cwd, ".omx", "state");
|
|
2164
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2165
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-
|
|
2166
|
-
await writeJson(join(stateDir, "sessions", "sess-
|
|
2482
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview"), { recursive: true });
|
|
2483
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview" });
|
|
2484
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "skill-active-state.json"), {
|
|
2167
2485
|
active: true,
|
|
2168
|
-
|
|
2169
|
-
|
|
2486
|
+
skill: "deep-interview",
|
|
2487
|
+
phase: "planning",
|
|
2488
|
+
});
|
|
2489
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
|
|
2490
|
+
active: true,
|
|
2491
|
+
current_phase: "planning",
|
|
2170
2492
|
});
|
|
2171
2493
|
const result = await dispatchCodexNativeHook({
|
|
2172
2494
|
hook_event_name: "Stop",
|
|
2173
2495
|
cwd,
|
|
2174
|
-
session_id: "sess-
|
|
2496
|
+
session_id: "sess-stop-deep-interview",
|
|
2175
2497
|
}, { cwd });
|
|
2176
|
-
assert.equal(result.
|
|
2177
|
-
assert.deepEqual(result.outputJson, {
|
|
2178
|
-
decision: "block",
|
|
2179
|
-
reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2180
|
-
stopReason: "ralph_executing",
|
|
2181
|
-
systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2182
|
-
});
|
|
2498
|
+
assert.equal(result.outputJson, null);
|
|
2183
2499
|
}
|
|
2184
2500
|
finally {
|
|
2185
2501
|
await rm(cwd, { recursive: true, force: true });
|
|
2186
2502
|
}
|
|
2187
2503
|
});
|
|
2188
|
-
it("
|
|
2189
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2504
|
+
it("blocks Stop when deep-interview has a pending omx question obligation", async () => {
|
|
2505
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-"));
|
|
2190
2506
|
try {
|
|
2191
2507
|
const stateDir = join(cwd, ".omx", "state");
|
|
2192
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2193
|
-
await
|
|
2194
|
-
await writeJson(join(stateDir, "
|
|
2195
|
-
|
|
2508
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question"), { recursive: true });
|
|
2509
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question" });
|
|
2510
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "skill-active-state.json"), {
|
|
2511
|
+
version: 1,
|
|
2196
2512
|
active: true,
|
|
2197
|
-
|
|
2198
|
-
|
|
2513
|
+
skill: "deep-interview",
|
|
2514
|
+
phase: "planning",
|
|
2515
|
+
session_id: "sess-stop-deep-interview-question",
|
|
2516
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
2517
|
+
});
|
|
2518
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "deep-interview-state.json"), {
|
|
2519
|
+
active: true,
|
|
2520
|
+
mode: "deep-interview",
|
|
2521
|
+
current_phase: "intent-first",
|
|
2522
|
+
session_id: "sess-stop-deep-interview-question",
|
|
2523
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
2524
|
+
question_enforcement: {
|
|
2525
|
+
obligation_id: "obligation-1",
|
|
2526
|
+
source: "omx-question",
|
|
2527
|
+
status: "pending",
|
|
2528
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
2529
|
+
},
|
|
2199
2530
|
});
|
|
2200
2531
|
const result = await dispatchCodexNativeHook({
|
|
2201
2532
|
hook_event_name: "Stop",
|
|
2202
2533
|
cwd,
|
|
2203
|
-
session_id: "sess-
|
|
2534
|
+
session_id: "sess-stop-deep-interview-question",
|
|
2535
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
2204
2536
|
}, { cwd });
|
|
2205
2537
|
assert.equal(result.omxEventName, "stop");
|
|
2206
|
-
assert.
|
|
2538
|
+
assert.deepEqual(result.outputJson, {
|
|
2539
|
+
decision: "block",
|
|
2540
|
+
reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
2541
|
+
stopReason: "deep_interview_question_required",
|
|
2542
|
+
systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
2543
|
+
});
|
|
2207
2544
|
}
|
|
2208
2545
|
finally {
|
|
2209
2546
|
await rm(cwd, { recursive: true, force: true });
|
|
2210
2547
|
}
|
|
2211
2548
|
});
|
|
2212
|
-
it("
|
|
2213
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2549
|
+
it("blocks Stop when a same-session deep-interview question obligation is pending even after the mode marked itself inactive", async () => {
|
|
2550
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-inactive-"));
|
|
2214
2551
|
try {
|
|
2215
2552
|
const stateDir = join(cwd, ".omx", "state");
|
|
2216
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2217
|
-
await writeJson(join(stateDir, "
|
|
2553
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive"), { recursive: true });
|
|
2554
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-inactive" });
|
|
2555
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive", "skill-active-state.json"), {
|
|
2556
|
+
version: 1,
|
|
2557
|
+
active: true,
|
|
2558
|
+
skill: "deep-interview",
|
|
2559
|
+
phase: "planning",
|
|
2560
|
+
session_id: "sess-stop-deep-interview-question-inactive",
|
|
2561
|
+
thread_id: "thread-stop-deep-interview-question-inactive",
|
|
2562
|
+
});
|
|
2563
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-inactive", "deep-interview-state.json"), {
|
|
2564
|
+
active: false,
|
|
2565
|
+
mode: "deep-interview",
|
|
2566
|
+
current_phase: "intent-first",
|
|
2567
|
+
lifecycle_outcome: "askuserQuestion",
|
|
2568
|
+
run_outcome: "blocked_on_user",
|
|
2569
|
+
completed_at: "2026-04-19T03:20:30.000Z",
|
|
2570
|
+
session_id: "sess-stop-deep-interview-question-inactive",
|
|
2571
|
+
thread_id: "thread-stop-deep-interview-question-inactive",
|
|
2572
|
+
question_enforcement: {
|
|
2573
|
+
obligation_id: "obligation-inactive",
|
|
2574
|
+
source: "omx-question",
|
|
2575
|
+
status: "pending",
|
|
2576
|
+
lifecycle_outcome: "askuserQuestion",
|
|
2577
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
2578
|
+
},
|
|
2579
|
+
});
|
|
2580
|
+
const result = await dispatchCodexNativeHook({
|
|
2581
|
+
hook_event_name: "Stop",
|
|
2582
|
+
cwd,
|
|
2583
|
+
session_id: "sess-stop-deep-interview-question-inactive",
|
|
2584
|
+
thread_id: "thread-stop-deep-interview-question-inactive",
|
|
2585
|
+
}, { cwd });
|
|
2586
|
+
assert.equal(result.omxEventName, "stop");
|
|
2587
|
+
assert.deepEqual(result.outputJson, {
|
|
2588
|
+
decision: "block",
|
|
2589
|
+
reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
2590
|
+
stopReason: "deep_interview_question_required",
|
|
2591
|
+
systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
finally {
|
|
2595
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
it("keeps blocking pending deep-interview question Stop replays until the obligation changes", async () => {
|
|
2599
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-replay-"));
|
|
2600
|
+
try {
|
|
2601
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2602
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay"), { recursive: true });
|
|
2603
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-replay" });
|
|
2604
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "skill-active-state.json"), {
|
|
2605
|
+
version: 1,
|
|
2606
|
+
active: true,
|
|
2607
|
+
skill: "deep-interview",
|
|
2608
|
+
phase: "planning",
|
|
2609
|
+
session_id: "sess-stop-deep-interview-question-replay",
|
|
2610
|
+
});
|
|
2611
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "deep-interview-state.json"), {
|
|
2612
|
+
active: true,
|
|
2613
|
+
mode: "deep-interview",
|
|
2614
|
+
current_phase: "intent-first",
|
|
2615
|
+
question_enforcement: {
|
|
2616
|
+
obligation_id: "obligation-replay",
|
|
2617
|
+
source: "omx-question",
|
|
2618
|
+
status: "pending",
|
|
2619
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
2620
|
+
},
|
|
2621
|
+
});
|
|
2622
|
+
const payload = {
|
|
2623
|
+
hook_event_name: "Stop",
|
|
2624
|
+
cwd,
|
|
2625
|
+
session_id: "sess-stop-deep-interview-question-replay",
|
|
2626
|
+
};
|
|
2627
|
+
const expected = {
|
|
2628
|
+
decision: "block",
|
|
2629
|
+
reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
2630
|
+
stopReason: "deep_interview_question_required",
|
|
2631
|
+
systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
2632
|
+
};
|
|
2633
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
2634
|
+
const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
|
|
2635
|
+
assert.equal(first.omxEventName, "stop");
|
|
2636
|
+
assert.deepEqual(first.outputJson, expected);
|
|
2637
|
+
assert.equal(replay.omxEventName, "stop");
|
|
2638
|
+
assert.deepEqual(replay.outputJson, expected);
|
|
2639
|
+
}
|
|
2640
|
+
finally {
|
|
2641
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
it("does not block Stop once the deep-interview question obligation is satisfied or cleared", async () => {
|
|
2645
|
+
for (const status of ["satisfied", "cleared"]) {
|
|
2646
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-deep-interview-question-${status}-`));
|
|
2647
|
+
try {
|
|
2648
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2649
|
+
await mkdir(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`), { recursive: true });
|
|
2650
|
+
await writeJson(join(stateDir, "session.json"), { session_id: `sess-stop-deep-interview-question-${status}` });
|
|
2651
|
+
await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "skill-active-state.json"), {
|
|
2652
|
+
version: 1,
|
|
2653
|
+
active: true,
|
|
2654
|
+
skill: "deep-interview",
|
|
2655
|
+
phase: "planning",
|
|
2656
|
+
session_id: `sess-stop-deep-interview-question-${status}`,
|
|
2657
|
+
});
|
|
2658
|
+
await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "deep-interview-state.json"), {
|
|
2659
|
+
active: true,
|
|
2660
|
+
mode: "deep-interview",
|
|
2661
|
+
current_phase: "intent-first",
|
|
2662
|
+
question_enforcement: {
|
|
2663
|
+
obligation_id: `obligation-${status}`,
|
|
2664
|
+
source: "omx-question",
|
|
2665
|
+
status,
|
|
2666
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
2667
|
+
...(status === "satisfied"
|
|
2668
|
+
? { question_id: "question-1", satisfied_at: "2026-04-19T03:21:00.000Z" }
|
|
2669
|
+
: { cleared_at: "2026-04-19T03:21:00.000Z", clear_reason: "error" }),
|
|
2670
|
+
},
|
|
2671
|
+
});
|
|
2672
|
+
const result = await dispatchCodexNativeHook({
|
|
2673
|
+
hook_event_name: "Stop",
|
|
2674
|
+
cwd,
|
|
2675
|
+
session_id: `sess-stop-deep-interview-question-${status}`,
|
|
2676
|
+
}, { cwd });
|
|
2677
|
+
assert.equal(result.omxEventName, "stop");
|
|
2678
|
+
assert.equal(result.outputJson, null);
|
|
2679
|
+
}
|
|
2680
|
+
finally {
|
|
2681
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
it("ignores pending deep-interview question obligations from another session", async () => {
|
|
2686
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-foreign-session-"));
|
|
2687
|
+
try {
|
|
2688
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2689
|
+
await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
|
|
2690
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
2691
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
2692
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "skill-active-state.json"), {
|
|
2693
|
+
version: 1,
|
|
2694
|
+
active: true,
|
|
2695
|
+
skill: "deep-interview",
|
|
2696
|
+
phase: "planning",
|
|
2697
|
+
session_id: "sess-other",
|
|
2698
|
+
});
|
|
2699
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "deep-interview-state.json"), {
|
|
2700
|
+
active: true,
|
|
2701
|
+
mode: "deep-interview",
|
|
2702
|
+
current_phase: "intent-first",
|
|
2703
|
+
question_enforcement: {
|
|
2704
|
+
obligation_id: "obligation-foreign",
|
|
2705
|
+
source: "omx-question",
|
|
2706
|
+
status: "pending",
|
|
2707
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
2708
|
+
},
|
|
2709
|
+
});
|
|
2710
|
+
const result = await dispatchCodexNativeHook({
|
|
2711
|
+
hook_event_name: "Stop",
|
|
2712
|
+
cwd,
|
|
2713
|
+
session_id: "sess-current",
|
|
2714
|
+
}, { cwd });
|
|
2715
|
+
assert.equal(result.omxEventName, "stop");
|
|
2716
|
+
assert.equal(result.outputJson, null);
|
|
2717
|
+
}
|
|
2718
|
+
finally {
|
|
2719
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
it("blocks a new same-session deep-interview question obligation even after an earlier round was satisfied", async () => {
|
|
2723
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-next-round-"));
|
|
2724
|
+
try {
|
|
2725
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2726
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round"), { recursive: true });
|
|
2727
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-next-round" });
|
|
2728
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "skill-active-state.json"), {
|
|
2729
|
+
version: 1,
|
|
2730
|
+
active: true,
|
|
2731
|
+
skill: "deep-interview",
|
|
2732
|
+
phase: "planning",
|
|
2733
|
+
session_id: "sess-stop-deep-interview-question-next-round",
|
|
2734
|
+
});
|
|
2735
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "deep-interview-state.json"), {
|
|
2736
|
+
active: true,
|
|
2737
|
+
mode: "deep-interview",
|
|
2738
|
+
current_phase: "intent-first",
|
|
2739
|
+
question_enforcement: {
|
|
2740
|
+
obligation_id: "obligation-next-round",
|
|
2741
|
+
source: "omx-question",
|
|
2742
|
+
status: "pending",
|
|
2743
|
+
requested_at: "2026-04-19T03:22:00.000Z",
|
|
2744
|
+
question_id: "question-old-round",
|
|
2745
|
+
satisfied_at: "2026-04-19T03:21:00.000Z",
|
|
2746
|
+
},
|
|
2747
|
+
});
|
|
2748
|
+
const result = await dispatchCodexNativeHook({
|
|
2749
|
+
hook_event_name: "Stop",
|
|
2750
|
+
cwd,
|
|
2751
|
+
session_id: "sess-stop-deep-interview-question-next-round",
|
|
2752
|
+
}, { cwd });
|
|
2753
|
+
assert.equal(result.omxEventName, "stop");
|
|
2754
|
+
assert.deepEqual(result.outputJson, {
|
|
2755
|
+
decision: "block",
|
|
2756
|
+
reason: "Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
2757
|
+
stopReason: "deep_interview_question_required",
|
|
2758
|
+
systemMessage: "OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
finally {
|
|
2762
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
it("ignores root skill-active fallback from a different thread when evaluating Stop", async () => {
|
|
2766
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-thread-"));
|
|
2767
|
+
try {
|
|
2768
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2769
|
+
await mkdir(stateDir, { recursive: true });
|
|
2770
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
2771
|
+
active: true,
|
|
2772
|
+
skill: "deep-interview",
|
|
2773
|
+
phase: "planning",
|
|
2774
|
+
session_id: "",
|
|
2775
|
+
thread_id: "other-thread",
|
|
2776
|
+
});
|
|
2777
|
+
const result = await dispatchCodexNativeHook({
|
|
2778
|
+
hook_event_name: "Stop",
|
|
2779
|
+
cwd,
|
|
2780
|
+
session_id: "sess-stop-main",
|
|
2781
|
+
thread_id: "main-thread",
|
|
2782
|
+
}, { cwd });
|
|
2783
|
+
assert.equal(result.outputJson, null);
|
|
2784
|
+
}
|
|
2785
|
+
finally {
|
|
2786
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
|
|
2790
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
|
|
2791
|
+
try {
|
|
2792
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2793
|
+
await mkdir(stateDir, { recursive: true });
|
|
2794
|
+
await writeFile(join(stateDir, "ralph-state.json"), JSON.stringify({
|
|
2795
|
+
active: true,
|
|
2796
|
+
current_phase: "executing",
|
|
2797
|
+
}));
|
|
2798
|
+
const result = await dispatchCodexNativeHook({
|
|
2799
|
+
hook_event_name: "Stop",
|
|
2800
|
+
cwd,
|
|
2801
|
+
}, { cwd });
|
|
2802
|
+
assert.equal(result.omxEventName, "stop");
|
|
2803
|
+
assert.deepEqual(result.outputJson, {
|
|
2804
|
+
decision: "block",
|
|
2805
|
+
reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2806
|
+
stopReason: "ralph_executing",
|
|
2807
|
+
systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
finally {
|
|
2811
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
it("blocks Stop from session-scoped Ralph state when session.json points to another session", async () => {
|
|
2815
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-session-mismatch-"));
|
|
2816
|
+
try {
|
|
2817
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2818
|
+
await mkdir(join(stateDir, "sessions", "sess-live-ralph"), { recursive: true });
|
|
2819
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-ralph" });
|
|
2820
|
+
await writeJson(join(stateDir, "sessions", "sess-live-ralph", "ralph-state.json"), {
|
|
2821
|
+
active: true,
|
|
2822
|
+
current_phase: "executing",
|
|
2823
|
+
session_id: "sess-live-ralph",
|
|
2824
|
+
});
|
|
2825
|
+
const result = await dispatchCodexNativeHook({
|
|
2826
|
+
hook_event_name: "Stop",
|
|
2827
|
+
cwd,
|
|
2828
|
+
session_id: "sess-live-ralph",
|
|
2829
|
+
}, { cwd });
|
|
2830
|
+
assert.equal(result.omxEventName, "stop");
|
|
2831
|
+
assert.deepEqual(result.outputJson, {
|
|
2832
|
+
decision: "block",
|
|
2833
|
+
reason: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2834
|
+
stopReason: "ralph_executing",
|
|
2835
|
+
systemMessage: "OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
finally {
|
|
2839
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
it("does not block Stop from stale session-scoped Ralph state that belongs to another session", async () => {
|
|
2843
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralph-"));
|
|
2844
|
+
try {
|
|
2845
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2846
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
2847
|
+
await mkdir(join(stateDir, "sessions", "sess-stale"), { recursive: true });
|
|
2848
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
2849
|
+
await writeJson(join(stateDir, "sessions", "sess-stale", "ralph-state.json"), {
|
|
2850
|
+
active: true,
|
|
2851
|
+
current_phase: "starting",
|
|
2852
|
+
session_id: "sess-stale",
|
|
2853
|
+
});
|
|
2854
|
+
const result = await dispatchCodexNativeHook({
|
|
2855
|
+
hook_event_name: "Stop",
|
|
2856
|
+
cwd,
|
|
2857
|
+
session_id: "sess-current",
|
|
2858
|
+
}, { cwd });
|
|
2859
|
+
assert.equal(result.omxEventName, "stop");
|
|
2860
|
+
assert.equal(result.outputJson, null);
|
|
2861
|
+
}
|
|
2862
|
+
finally {
|
|
2863
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
it("does not block Stop from stale current-session Ralph state when session.json points to a dead owner", async () => {
|
|
2867
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-current-session-ralph-"));
|
|
2868
|
+
try {
|
|
2869
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2870
|
+
await mkdir(join(stateDir, "sessions", "sess-dead"), { recursive: true });
|
|
2871
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2872
|
+
session_id: "sess-dead",
|
|
2873
|
+
cwd,
|
|
2874
|
+
pid: Number.MAX_SAFE_INTEGER,
|
|
2875
|
+
started_at: "2026-01-01T00:00:00.000Z",
|
|
2876
|
+
});
|
|
2877
|
+
await writeJson(join(stateDir, "sessions", "sess-dead", "ralph-state.json"), {
|
|
2878
|
+
active: true,
|
|
2879
|
+
current_phase: "verifying",
|
|
2880
|
+
session_id: "sess-dead",
|
|
2881
|
+
});
|
|
2882
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
2883
|
+
active: true,
|
|
2884
|
+
skill: "team",
|
|
2885
|
+
phase: "team-exec",
|
|
2886
|
+
active_skills: [{ skill: "team", phase: "team-exec", active: true, session_id: "sess-dead" }],
|
|
2887
|
+
});
|
|
2888
|
+
await writeJson(join(stateDir, "native-stop-state.json"), {
|
|
2889
|
+
sessions: {
|
|
2890
|
+
"sess-dead": {
|
|
2891
|
+
last_signature: "ralph-stop|sess-dead|thread-1|no-message|verifying",
|
|
2892
|
+
updated_at: "2026-04-20T21:00:00.000Z",
|
|
2893
|
+
},
|
|
2894
|
+
},
|
|
2895
|
+
});
|
|
2896
|
+
const result = await dispatchCodexNativeHook({
|
|
2897
|
+
hook_event_name: "Stop",
|
|
2898
|
+
cwd,
|
|
2899
|
+
session_id: "sess-dead",
|
|
2900
|
+
thread_id: "thread-1",
|
|
2901
|
+
stop_hook_active: true,
|
|
2902
|
+
}, { cwd });
|
|
2903
|
+
assert.equal(result.omxEventName, "stop");
|
|
2904
|
+
assert.equal(result.outputJson, null);
|
|
2905
|
+
}
|
|
2906
|
+
finally {
|
|
2907
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2908
|
+
}
|
|
2909
|
+
});
|
|
2910
|
+
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
2911
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
2912
|
+
try {
|
|
2913
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2914
|
+
await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
|
|
2915
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "ralph-state.json"), {
|
|
2218
2916
|
active: true,
|
|
2219
2917
|
current_phase: "starting",
|
|
2220
2918
|
session_id: "sess-other",
|
|
@@ -2346,6 +3044,91 @@ esac
|
|
|
2346
3044
|
await rm(cwd, { recursive: true, force: true });
|
|
2347
3045
|
}
|
|
2348
3046
|
});
|
|
3047
|
+
it("lets dispatcher dedupe identical native stop hook replays after Stop payload normalization", async () => {
|
|
3048
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-hook-dedupe-"));
|
|
3049
|
+
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3050
|
+
try {
|
|
3051
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3052
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ralph-hook-dedupe"), { recursive: true });
|
|
3053
|
+
await writeHookCounterPlugin(cwd);
|
|
3054
|
+
await writeFile(join(stateDir, "sessions", "sess-stop-ralph-hook-dedupe", "ralph-state.json"), JSON.stringify({
|
|
3055
|
+
active: true,
|
|
3056
|
+
current_phase: "executing",
|
|
3057
|
+
session_id: "sess-stop-ralph-hook-dedupe",
|
|
3058
|
+
}));
|
|
3059
|
+
process.env.OMX_SESSION_ID = "sess-stop-ralph-hook-dedupe";
|
|
3060
|
+
const payload = {
|
|
3061
|
+
hook_event_name: "Stop",
|
|
3062
|
+
cwd,
|
|
3063
|
+
session_id: "sess-stop-ralph-hook-dedupe",
|
|
3064
|
+
thread_id: "thread-stop-ralph-hook-dedupe",
|
|
3065
|
+
turn_id: "turn-stop-ralph-hook-dedupe-1",
|
|
3066
|
+
last_assistant_message: "Next active targets:\n\n1. scheduler integration\n\nI am continuing.",
|
|
3067
|
+
};
|
|
3068
|
+
await dispatchCodexNativeHook(payload, { cwd });
|
|
3069
|
+
await dispatchCodexNativeHook({
|
|
3070
|
+
...payload,
|
|
3071
|
+
stop_hook_active: true,
|
|
3072
|
+
}, { cwd });
|
|
3073
|
+
const marker = JSON.parse(await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"));
|
|
3074
|
+
assert.equal(marker.count, 1);
|
|
3075
|
+
}
|
|
3076
|
+
finally {
|
|
3077
|
+
if (typeof previousOmxSessionId === "string")
|
|
3078
|
+
process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3079
|
+
else
|
|
3080
|
+
delete process.env.OMX_SESSION_ID;
|
|
3081
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3082
|
+
}
|
|
3083
|
+
});
|
|
3084
|
+
it("preserves per-turn native stop hook delivery even when stop_hook_active remains true", async () => {
|
|
3085
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-hook-refire-"));
|
|
3086
|
+
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3087
|
+
try {
|
|
3088
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3089
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ralph-hook-refire"), { recursive: true });
|
|
3090
|
+
await writeHookCounterPlugin(cwd);
|
|
3091
|
+
await writeFile(join(stateDir, "sessions", "sess-stop-ralph-hook-refire", "ralph-state.json"), JSON.stringify({
|
|
3092
|
+
active: true,
|
|
3093
|
+
current_phase: "executing",
|
|
3094
|
+
session_id: "sess-stop-ralph-hook-refire",
|
|
3095
|
+
}));
|
|
3096
|
+
process.env.OMX_SESSION_ID = "sess-stop-ralph-hook-refire";
|
|
3097
|
+
const payload = {
|
|
3098
|
+
hook_event_name: "Stop",
|
|
3099
|
+
cwd,
|
|
3100
|
+
session_id: "sess-stop-ralph-hook-refire",
|
|
3101
|
+
thread_id: "thread-stop-ralph-hook-refire",
|
|
3102
|
+
turn_id: "turn-stop-ralph-hook-refire-1",
|
|
3103
|
+
last_assistant_message: "Continuing current task.",
|
|
3104
|
+
};
|
|
3105
|
+
await dispatchCodexNativeHook(payload, { cwd });
|
|
3106
|
+
await dispatchCodexNativeHook({
|
|
3107
|
+
...payload,
|
|
3108
|
+
turn_id: "turn-stop-ralph-hook-refire-2",
|
|
3109
|
+
stop_hook_active: true,
|
|
3110
|
+
}, { cwd });
|
|
3111
|
+
await writeFile(join(stateDir, "sessions", "sess-stop-ralph-hook-refire", "ralph-state.json"), JSON.stringify({
|
|
3112
|
+
active: true,
|
|
3113
|
+
current_phase: "executing",
|
|
3114
|
+
session_id: "sess-stop-ralph-hook-refire",
|
|
3115
|
+
}));
|
|
3116
|
+
await dispatchCodexNativeHook({
|
|
3117
|
+
...payload,
|
|
3118
|
+
turn_id: "turn-stop-ralph-hook-refire-3",
|
|
3119
|
+
stop_hook_active: true,
|
|
3120
|
+
}, { cwd });
|
|
3121
|
+
const marker = JSON.parse(await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"));
|
|
3122
|
+
assert.equal(marker.count, 3);
|
|
3123
|
+
}
|
|
3124
|
+
finally {
|
|
3125
|
+
if (typeof previousOmxSessionId === "string")
|
|
3126
|
+
process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3127
|
+
else
|
|
3128
|
+
delete process.env.OMX_SESSION_ID;
|
|
3129
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
2349
3132
|
it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
|
|
2350
3133
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
2351
3134
|
try {
|
|
@@ -2446,6 +3229,51 @@ esac
|
|
|
2446
3229
|
await rm(cwd, { recursive: true, force: true });
|
|
2447
3230
|
}
|
|
2448
3231
|
});
|
|
3232
|
+
it("dedupes native stop hook replay across owner launch SessionStart reconciliation drift", async () => {
|
|
3233
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-dispatch-session-drift-"));
|
|
3234
|
+
try {
|
|
3235
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3236
|
+
await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
|
|
3237
|
+
await writeHookCounterPlugin(cwd);
|
|
3238
|
+
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
3239
|
+
await writeSessionStart(cwd, "omx-canonical");
|
|
3240
|
+
await writeJson(join(stateDir, "sessions", "omx-canonical", "ralph-state.json"), {
|
|
3241
|
+
active: true,
|
|
3242
|
+
current_phase: "executing",
|
|
3243
|
+
session_id: "omx-canonical",
|
|
3244
|
+
});
|
|
3245
|
+
await dispatchCodexNativeHook({
|
|
3246
|
+
hook_event_name: "SessionStart",
|
|
3247
|
+
cwd,
|
|
3248
|
+
session_id: "codex-native-new",
|
|
3249
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
3250
|
+
await dispatchCodexNativeHook({
|
|
3251
|
+
hook_event_name: "Stop",
|
|
3252
|
+
cwd,
|
|
3253
|
+
session_id: "codex-native-new",
|
|
3254
|
+
thread_id: "thread-stop-hook-drift",
|
|
3255
|
+
turn_id: "turn-stop-hook-drift-1",
|
|
3256
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3257
|
+
}, { cwd });
|
|
3258
|
+
await dispatchCodexNativeHook({
|
|
3259
|
+
hook_event_name: "Stop",
|
|
3260
|
+
cwd,
|
|
3261
|
+
session_id: "omx-canonical",
|
|
3262
|
+
thread_id: "thread-stop-hook-drift",
|
|
3263
|
+
turn_id: "turn-stop-hook-drift-1",
|
|
3264
|
+
stop_hook_active: true,
|
|
3265
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3266
|
+
}, { cwd });
|
|
3267
|
+
const marker = JSON.parse(await readFile(join(cwd, ".omx", "stop-hook-counter.json"), "utf-8"));
|
|
3268
|
+
assert.equal(marker.count, 1);
|
|
3269
|
+
const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
|
|
3270
|
+
assert.equal(sessionState.session_id, "omx-canonical");
|
|
3271
|
+
assert.equal(sessionState.native_session_id, "codex-native-new");
|
|
3272
|
+
}
|
|
3273
|
+
finally {
|
|
3274
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3275
|
+
}
|
|
3276
|
+
});
|
|
2449
3277
|
it("re-fires native auto-nudge for a later fresh Stop reply even when stop_hook_active is true", async () => {
|
|
2450
3278
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-refire-"));
|
|
2451
3279
|
try {
|
|
@@ -2903,61 +3731,52 @@ esac
|
|
|
2903
3731
|
await rm(cwd, { recursive: true, force: true });
|
|
2904
3732
|
}
|
|
2905
3733
|
});
|
|
2906
|
-
it("
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
stopReason: `${testCase.mode}_${testCase.phase}`,
|
|
2953
|
-
systemMessage: `OMX ${testCase.mode} is still active (phase: ${testCase.phase}).`,
|
|
2954
|
-
});
|
|
2955
|
-
}
|
|
2956
|
-
finally {
|
|
2957
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
});
|
|
3734
|
+
it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
|
|
3735
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
|
|
3736
|
+
try {
|
|
3737
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3738
|
+
await mkdir(stateDir, { recursive: true });
|
|
3739
|
+
await writeJson(join(stateDir, "ultrawork-state.json"), {
|
|
3740
|
+
active: true,
|
|
3741
|
+
current_phase: "executing",
|
|
3742
|
+
});
|
|
3743
|
+
const first = await dispatchCodexNativeHook({
|
|
3744
|
+
hook_event_name: "Stop",
|
|
3745
|
+
cwd,
|
|
3746
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
3747
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
3748
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
3749
|
+
}, { cwd });
|
|
3750
|
+
const repeated = await dispatchCodexNativeHook({
|
|
3751
|
+
hook_event_name: "Stop",
|
|
3752
|
+
cwd,
|
|
3753
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
3754
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
3755
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
3756
|
+
stop_hook_active: true,
|
|
3757
|
+
}, { cwd });
|
|
3758
|
+
const fresh = await dispatchCodexNativeHook({
|
|
3759
|
+
hook_event_name: "Stop",
|
|
3760
|
+
cwd,
|
|
3761
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
3762
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
3763
|
+
turn_id: "turn-stop-ultrawork-repeat-2",
|
|
3764
|
+
stop_hook_active: true,
|
|
3765
|
+
}, { cwd });
|
|
3766
|
+
assert.equal(first.omxEventName, "stop");
|
|
3767
|
+
assert.deepEqual(repeated.outputJson, null);
|
|
3768
|
+
assert.equal(fresh.omxEventName, "stop");
|
|
3769
|
+
assert.deepEqual(fresh.outputJson, {
|
|
3770
|
+
decision: "block",
|
|
3771
|
+
reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3772
|
+
stopReason: "ultrawork_executing",
|
|
3773
|
+
systemMessage: "OMX ultrawork is still active (phase: executing).",
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
finally {
|
|
3777
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
2961
3780
|
it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
|
|
2962
3781
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
|
|
2963
3782
|
try {
|
|
@@ -3142,4 +3961,518 @@ esac
|
|
|
3142
3961
|
}
|
|
3143
3962
|
});
|
|
3144
3963
|
});
|
|
3964
|
+
// ---------------------------------------------------------------------------
|
|
3965
|
+
// Triage layer integration tests
|
|
3966
|
+
// ---------------------------------------------------------------------------
|
|
3967
|
+
describe("codex native hook triage integration", () => {
|
|
3968
|
+
const priorCodexHome = process.env.CODEX_HOME;
|
|
3969
|
+
beforeEach(() => {
|
|
3970
|
+
resetTriageConfigCache();
|
|
3971
|
+
});
|
|
3972
|
+
afterEach(() => {
|
|
3973
|
+
if (typeof priorCodexHome === "string")
|
|
3974
|
+
process.env.CODEX_HOME = priorCodexHome;
|
|
3975
|
+
else
|
|
3976
|
+
delete process.env.CODEX_HOME;
|
|
3977
|
+
resetTriageConfigCache();
|
|
3978
|
+
});
|
|
3979
|
+
// ── Group 1: Keyword bypass (triage must NOT run) ────────────────────────
|
|
3980
|
+
it("does not inject triage advisory for $ralplan keyword prompts", async () => {
|
|
3981
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-ralplan-"));
|
|
3982
|
+
try {
|
|
3983
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
3984
|
+
const result = await dispatchCodexNativeHook({
|
|
3985
|
+
hook_event_name: "UserPromptSubmit",
|
|
3986
|
+
cwd,
|
|
3987
|
+
session_id: "triage-kw-ralplan-1",
|
|
3988
|
+
thread_id: "thread-triage-kw-1",
|
|
3989
|
+
turn_id: "turn-triage-kw-1",
|
|
3990
|
+
prompt: "$ralplan implement issue #1307",
|
|
3991
|
+
}, { cwd });
|
|
3992
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
3993
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
3994
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
3995
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
3996
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
3997
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-ralplan-1", "prompt-routing-state.json");
|
|
3998
|
+
assert.equal(existsSync(stateFile), false);
|
|
3999
|
+
}
|
|
4000
|
+
finally {
|
|
4001
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4002
|
+
}
|
|
4003
|
+
});
|
|
4004
|
+
it("does not inject triage advisory for autopilot keyword prompts", async () => {
|
|
4005
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-autopilot-"));
|
|
4006
|
+
try {
|
|
4007
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4008
|
+
const result = await dispatchCodexNativeHook({
|
|
4009
|
+
hook_event_name: "UserPromptSubmit",
|
|
4010
|
+
cwd,
|
|
4011
|
+
session_id: "triage-kw-autopilot-1",
|
|
4012
|
+
thread_id: "thread-triage-kw-ap-1",
|
|
4013
|
+
turn_id: "turn-triage-kw-ap-1",
|
|
4014
|
+
prompt: "$autopilot build this",
|
|
4015
|
+
}, { cwd });
|
|
4016
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4017
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4018
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4019
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
4020
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4021
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-autopilot-1", "prompt-routing-state.json");
|
|
4022
|
+
assert.equal(existsSync(stateFile), false);
|
|
4023
|
+
}
|
|
4024
|
+
finally {
|
|
4025
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4026
|
+
}
|
|
4027
|
+
});
|
|
4028
|
+
// ── Group 2: HEAVY injection ─────────────────────────────────────────────
|
|
4029
|
+
it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
|
|
4030
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));
|
|
4031
|
+
try {
|
|
4032
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4033
|
+
const result = await dispatchCodexNativeHook({
|
|
4034
|
+
hook_event_name: "UserPromptSubmit",
|
|
4035
|
+
cwd,
|
|
4036
|
+
session_id: "triage-heavy-1",
|
|
4037
|
+
thread_id: "thread-triage-heavy-1",
|
|
4038
|
+
turn_id: "turn-triage-heavy-1",
|
|
4039
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4040
|
+
}, { cwd });
|
|
4041
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4042
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4043
|
+
assert.match(additionalContext, /Prefer the existing autopilot-style workflow/);
|
|
4044
|
+
// skill-active-state.json must NOT be written (triage is advisory only)
|
|
4045
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
4046
|
+
// prompt-routing-state.json must be written with lane=HEAVY
|
|
4047
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-heavy-1", "prompt-routing-state.json");
|
|
4048
|
+
assert.equal(existsSync(stateFile), true);
|
|
4049
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8"));
|
|
4050
|
+
assert.equal(state.version, 1);
|
|
4051
|
+
assert.equal(state.last_triage?.lane, "HEAVY");
|
|
4052
|
+
assert.equal(state.last_triage?.destination, "autopilot");
|
|
4053
|
+
assert.equal(state.suppress_followup, true);
|
|
4054
|
+
}
|
|
4055
|
+
finally {
|
|
4056
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
// ── Group 3: LIGHT/explore ────────────────────────────────────────────────
|
|
4060
|
+
it("injects LIGHT/explore advisory and writes state for a question-shaped prompt", async () => {
|
|
4061
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-explore-"));
|
|
4062
|
+
try {
|
|
4063
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4064
|
+
const result = await dispatchCodexNativeHook({
|
|
4065
|
+
hook_event_name: "UserPromptSubmit",
|
|
4066
|
+
cwd,
|
|
4067
|
+
session_id: "triage-explore-1",
|
|
4068
|
+
thread_id: "thread-triage-explore-1",
|
|
4069
|
+
turn_id: "turn-triage-explore-1",
|
|
4070
|
+
prompt: "explain this function",
|
|
4071
|
+
}, { cwd });
|
|
4072
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4073
|
+
assert.match(additionalContext, /read-only\/question-shaped/);
|
|
4074
|
+
assert.match(additionalContext, /Prefer the explore role surface/);
|
|
4075
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-explore-1", "prompt-routing-state.json");
|
|
4076
|
+
assert.equal(existsSync(stateFile), true);
|
|
4077
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8"));
|
|
4078
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
4079
|
+
assert.equal(state.last_triage?.destination, "explore");
|
|
4080
|
+
assert.equal(state.suppress_followup, true);
|
|
4081
|
+
}
|
|
4082
|
+
finally {
|
|
4083
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4084
|
+
}
|
|
4085
|
+
});
|
|
4086
|
+
// ── Group 4: LIGHT/executor ───────────────────────────────────────────────
|
|
4087
|
+
it("injects LIGHT/executor advisory and writes state for a narrow edit-shaped prompt", async () => {
|
|
4088
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-executor-"));
|
|
4089
|
+
try {
|
|
4090
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4091
|
+
const result = await dispatchCodexNativeHook({
|
|
4092
|
+
hook_event_name: "UserPromptSubmit",
|
|
4093
|
+
cwd,
|
|
4094
|
+
session_id: "triage-executor-1",
|
|
4095
|
+
thread_id: "thread-triage-executor-1",
|
|
4096
|
+
turn_id: "turn-triage-executor-1",
|
|
4097
|
+
prompt: "fix typo in src/foo.ts",
|
|
4098
|
+
}, { cwd });
|
|
4099
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4100
|
+
assert.match(additionalContext, /narrow edit-shaped/);
|
|
4101
|
+
assert.match(additionalContext, /Prefer the executor role surface/);
|
|
4102
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-executor-1", "prompt-routing-state.json");
|
|
4103
|
+
assert.equal(existsSync(stateFile), true);
|
|
4104
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8"));
|
|
4105
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
4106
|
+
assert.equal(state.last_triage?.destination, "executor");
|
|
4107
|
+
}
|
|
4108
|
+
finally {
|
|
4109
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4110
|
+
}
|
|
4111
|
+
});
|
|
4112
|
+
// ── Group 5: LIGHT/designer ───────────────────────────────────────────────
|
|
4113
|
+
it("injects LIGHT/designer advisory and writes state for a visual/style prompt", async () => {
|
|
4114
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-designer-"));
|
|
4115
|
+
try {
|
|
4116
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4117
|
+
const result = await dispatchCodexNativeHook({
|
|
4118
|
+
hook_event_name: "UserPromptSubmit",
|
|
4119
|
+
cwd,
|
|
4120
|
+
session_id: "triage-designer-1",
|
|
4121
|
+
thread_id: "thread-triage-designer-1",
|
|
4122
|
+
turn_id: "turn-triage-designer-1",
|
|
4123
|
+
prompt: "make the button blue",
|
|
4124
|
+
}, { cwd });
|
|
4125
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4126
|
+
assert.match(additionalContext, /visual\/style request/);
|
|
4127
|
+
assert.match(additionalContext, /Prefer the designer role surface/);
|
|
4128
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-designer-1", "prompt-routing-state.json");
|
|
4129
|
+
assert.equal(existsSync(stateFile), true);
|
|
4130
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8"));
|
|
4131
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
4132
|
+
assert.equal(state.last_triage?.destination, "designer");
|
|
4133
|
+
}
|
|
4134
|
+
finally {
|
|
4135
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4136
|
+
}
|
|
4137
|
+
});
|
|
4138
|
+
// ── Group 6: PASS (no triage injection, no state) ────────────────────────
|
|
4139
|
+
it("produces no triage advisory and no state for trivial greeting prompts", async () => {
|
|
4140
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-hello-"));
|
|
4141
|
+
try {
|
|
4142
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4143
|
+
const result = await dispatchCodexNativeHook({
|
|
4144
|
+
hook_event_name: "UserPromptSubmit",
|
|
4145
|
+
cwd,
|
|
4146
|
+
session_id: "triage-pass-hello-1",
|
|
4147
|
+
thread_id: "thread-triage-pass-1",
|
|
4148
|
+
turn_id: "turn-triage-pass-1",
|
|
4149
|
+
prompt: "hello",
|
|
4150
|
+
}, { cwd });
|
|
4151
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4152
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4153
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4154
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
4155
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4156
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-hello-1", "prompt-routing-state.json");
|
|
4157
|
+
assert.equal(existsSync(stateFile), false);
|
|
4158
|
+
}
|
|
4159
|
+
finally {
|
|
4160
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4161
|
+
}
|
|
4162
|
+
});
|
|
4163
|
+
it("produces no triage advisory and no state for ambiguous short prompts", async () => {
|
|
4164
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-short-"));
|
|
4165
|
+
try {
|
|
4166
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4167
|
+
const result = await dispatchCodexNativeHook({
|
|
4168
|
+
hook_event_name: "UserPromptSubmit",
|
|
4169
|
+
cwd,
|
|
4170
|
+
session_id: "triage-pass-short-1",
|
|
4171
|
+
thread_id: "thread-triage-pass-short-1",
|
|
4172
|
+
turn_id: "turn-triage-pass-short-1",
|
|
4173
|
+
prompt: "fix the thing",
|
|
4174
|
+
}, { cwd });
|
|
4175
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4176
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4177
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4178
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
4179
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4180
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-short-1", "prompt-routing-state.json");
|
|
4181
|
+
assert.equal(existsSync(stateFile), false);
|
|
4182
|
+
}
|
|
4183
|
+
finally {
|
|
4184
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4185
|
+
}
|
|
4186
|
+
});
|
|
4187
|
+
// ── Group 7: Turn-2 suppression (same session across two invocations) ────
|
|
4188
|
+
it("suppresses HEAVY triage re-injection on a short follow-up in the same session", async () => {
|
|
4189
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-heavy-"));
|
|
4190
|
+
const sessionId = "triage-suppress-heavy-1";
|
|
4191
|
+
try {
|
|
4192
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4193
|
+
// Turn 1: HEAVY fires
|
|
4194
|
+
const turn1 = await dispatchCodexNativeHook({
|
|
4195
|
+
hook_event_name: "UserPromptSubmit",
|
|
4196
|
+
cwd,
|
|
4197
|
+
session_id: sessionId,
|
|
4198
|
+
thread_id: "thread-suppress-heavy-1",
|
|
4199
|
+
turn_id: "turn-suppress-heavy-1",
|
|
4200
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4201
|
+
}, { cwd });
|
|
4202
|
+
const ctx1 = String(turn1.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4203
|
+
assert.match(ctx1, /multi-step goal with no workflow keyword/);
|
|
4204
|
+
// Turn 2: short follow-up — triage suppressed
|
|
4205
|
+
const turn2 = await dispatchCodexNativeHook({
|
|
4206
|
+
hook_event_name: "UserPromptSubmit",
|
|
4207
|
+
cwd,
|
|
4208
|
+
session_id: sessionId,
|
|
4209
|
+
thread_id: "thread-suppress-heavy-1",
|
|
4210
|
+
turn_id: "turn-suppress-heavy-2",
|
|
4211
|
+
prompt: "yes, settings page",
|
|
4212
|
+
}, { cwd });
|
|
4213
|
+
const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4214
|
+
assert.doesNotMatch(ctx2, /multi-step goal/);
|
|
4215
|
+
}
|
|
4216
|
+
finally {
|
|
4217
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4218
|
+
}
|
|
4219
|
+
});
|
|
4220
|
+
it("suppresses LIGHT/explore triage re-injection on a short follow-up in the same session", async () => {
|
|
4221
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-explore-"));
|
|
4222
|
+
const sessionId = "triage-suppress-explore-1";
|
|
4223
|
+
try {
|
|
4224
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4225
|
+
// Turn 1: LIGHT/explore fires
|
|
4226
|
+
await dispatchCodexNativeHook({
|
|
4227
|
+
hook_event_name: "UserPromptSubmit",
|
|
4228
|
+
cwd,
|
|
4229
|
+
session_id: sessionId,
|
|
4230
|
+
thread_id: "thread-suppress-explore-1",
|
|
4231
|
+
turn_id: "turn-suppress-explore-1",
|
|
4232
|
+
prompt: "explain this function",
|
|
4233
|
+
}, { cwd });
|
|
4234
|
+
// Turn 2: short follow-up — no duplicate LIGHT injection
|
|
4235
|
+
const turn2 = await dispatchCodexNativeHook({
|
|
4236
|
+
hook_event_name: "UserPromptSubmit",
|
|
4237
|
+
cwd,
|
|
4238
|
+
session_id: sessionId,
|
|
4239
|
+
thread_id: "thread-suppress-explore-1",
|
|
4240
|
+
turn_id: "turn-suppress-explore-2",
|
|
4241
|
+
prompt: "the auth helper",
|
|
4242
|
+
}, { cwd });
|
|
4243
|
+
const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4244
|
+
assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
|
|
4245
|
+
}
|
|
4246
|
+
finally {
|
|
4247
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4248
|
+
}
|
|
4249
|
+
});
|
|
4250
|
+
// ── Group 8: First-turn PASS does NOT block later triage ─────────────────
|
|
4251
|
+
it("still applies triage on turn 2 when turn 1 was a PASS with no state written", async () => {
|
|
4252
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-then-light-"));
|
|
4253
|
+
const sessionId = "triage-pass-then-light-1";
|
|
4254
|
+
try {
|
|
4255
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4256
|
+
// Turn 1: PASS — no state written
|
|
4257
|
+
await dispatchCodexNativeHook({
|
|
4258
|
+
hook_event_name: "UserPromptSubmit",
|
|
4259
|
+
cwd,
|
|
4260
|
+
session_id: sessionId,
|
|
4261
|
+
thread_id: "thread-pass-then-light-1",
|
|
4262
|
+
turn_id: "turn-pass-then-light-1",
|
|
4263
|
+
prompt: "hello",
|
|
4264
|
+
}, { cwd });
|
|
4265
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json")), false);
|
|
4266
|
+
// Turn 2: LIGHT/executor should fire normally
|
|
4267
|
+
const turn2 = await dispatchCodexNativeHook({
|
|
4268
|
+
hook_event_name: "UserPromptSubmit",
|
|
4269
|
+
cwd,
|
|
4270
|
+
session_id: sessionId,
|
|
4271
|
+
thread_id: "thread-pass-then-light-1",
|
|
4272
|
+
turn_id: "turn-pass-then-light-2",
|
|
4273
|
+
prompt: "fix typo in src/foo.ts",
|
|
4274
|
+
}, { cwd });
|
|
4275
|
+
const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4276
|
+
assert.match(ctx2, /narrow edit-shaped/);
|
|
4277
|
+
}
|
|
4278
|
+
finally {
|
|
4279
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4280
|
+
}
|
|
4281
|
+
});
|
|
4282
|
+
// ── Group 9: Opt-out forces PASS ─────────────────────────────────────────
|
|
4283
|
+
it("produces no triage advisory when prompt contains 'just chat' opt-out", async () => {
|
|
4284
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-chat-"));
|
|
4285
|
+
try {
|
|
4286
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4287
|
+
const result = await dispatchCodexNativeHook({
|
|
4288
|
+
hook_event_name: "UserPromptSubmit",
|
|
4289
|
+
cwd,
|
|
4290
|
+
session_id: "triage-optout-chat-1",
|
|
4291
|
+
thread_id: "thread-optout-chat-1",
|
|
4292
|
+
turn_id: "turn-optout-chat-1",
|
|
4293
|
+
prompt: "add dark mode toggle to the settings page, but just chat about it",
|
|
4294
|
+
}, { cwd });
|
|
4295
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4296
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4297
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4298
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-chat-1", "prompt-routing-state.json");
|
|
4299
|
+
assert.equal(existsSync(stateFile), false);
|
|
4300
|
+
}
|
|
4301
|
+
finally {
|
|
4302
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4305
|
+
it("produces no triage advisory when prompt contains 'no workflow' opt-out", async () => {
|
|
4306
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-noworkflow-"));
|
|
4307
|
+
try {
|
|
4308
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4309
|
+
const result = await dispatchCodexNativeHook({
|
|
4310
|
+
hook_event_name: "UserPromptSubmit",
|
|
4311
|
+
cwd,
|
|
4312
|
+
session_id: "triage-optout-noworkflow-1",
|
|
4313
|
+
thread_id: "thread-optout-noworkflow-1",
|
|
4314
|
+
turn_id: "turn-optout-noworkflow-1",
|
|
4315
|
+
prompt: "make the button blue, no workflow",
|
|
4316
|
+
}, { cwd });
|
|
4317
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4318
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4319
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-noworkflow-1", "prompt-routing-state.json");
|
|
4320
|
+
assert.equal(existsSync(stateFile), false);
|
|
4321
|
+
}
|
|
4322
|
+
finally {
|
|
4323
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4324
|
+
}
|
|
4325
|
+
});
|
|
4326
|
+
// ── Group 10: Keyword on follow-up turn wins cleanly ─────────────────────
|
|
4327
|
+
it("keyword on turn 2 suppresses triage and writes no triage state", async () => {
|
|
4328
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-kw-followup-"));
|
|
4329
|
+
const sessionId = "triage-kw-followup-1";
|
|
4330
|
+
try {
|
|
4331
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4332
|
+
// Turn 1: neutral prompt — triage may or may not fire, doesn't matter
|
|
4333
|
+
await dispatchCodexNativeHook({
|
|
4334
|
+
hook_event_name: "UserPromptSubmit",
|
|
4335
|
+
cwd,
|
|
4336
|
+
session_id: sessionId,
|
|
4337
|
+
thread_id: "thread-kw-followup-1",
|
|
4338
|
+
turn_id: "turn-kw-followup-1",
|
|
4339
|
+
prompt: "hello",
|
|
4340
|
+
}, { cwd });
|
|
4341
|
+
// Turn 2: keyword prompt — keyword fast-path runs, triage does NOT add extra advisory
|
|
4342
|
+
const turn2 = await dispatchCodexNativeHook({
|
|
4343
|
+
hook_event_name: "UserPromptSubmit",
|
|
4344
|
+
cwd,
|
|
4345
|
+
session_id: sessionId,
|
|
4346
|
+
thread_id: "thread-kw-followup-1",
|
|
4347
|
+
turn_id: "turn-kw-followup-2",
|
|
4348
|
+
prompt: "$ralph continue",
|
|
4349
|
+
}, { cwd });
|
|
4350
|
+
assert.equal(turn2.skillState?.skill, "ralph");
|
|
4351
|
+
const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4352
|
+
assert.doesNotMatch(ctx2, /multi-step goal with no workflow keyword/);
|
|
4353
|
+
assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
|
|
4354
|
+
assert.doesNotMatch(ctx2, /narrow edit-shaped/);
|
|
4355
|
+
assert.doesNotMatch(ctx2, /visual\/style request/);
|
|
4356
|
+
// No triage state written on the keyword turn
|
|
4357
|
+
const triageState = join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json");
|
|
4358
|
+
// The state from turn 1 (if any) must not have been created either (hello = PASS)
|
|
4359
|
+
assert.equal(existsSync(triageState), false);
|
|
4360
|
+
}
|
|
4361
|
+
finally {
|
|
4362
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4363
|
+
}
|
|
4364
|
+
});
|
|
4365
|
+
// ── Group 11: Config-disabled path ───────────────────────────────────────
|
|
4366
|
+
it("produces no triage advisory and no state when triage is disabled in config", async () => {
|
|
4367
|
+
const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-home-"));
|
|
4368
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-cwd-"));
|
|
4369
|
+
try {
|
|
4370
|
+
// Write a .omx-config.json in the fake CODEX_HOME that disables triage
|
|
4371
|
+
await writeJson(join(tmpHome, ".omx-config.json"), {
|
|
4372
|
+
promptRouting: { triage: { enabled: false } },
|
|
4373
|
+
});
|
|
4374
|
+
process.env.CODEX_HOME = tmpHome;
|
|
4375
|
+
resetTriageConfigCache();
|
|
4376
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4377
|
+
const result = await dispatchCodexNativeHook({
|
|
4378
|
+
hook_event_name: "UserPromptSubmit",
|
|
4379
|
+
cwd,
|
|
4380
|
+
session_id: "triage-disabled-1",
|
|
4381
|
+
thread_id: "thread-triage-disabled-1",
|
|
4382
|
+
turn_id: "turn-triage-disabled-1",
|
|
4383
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4384
|
+
}, { cwd });
|
|
4385
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4386
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4387
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-disabled-1", "prompt-routing-state.json");
|
|
4388
|
+
assert.equal(existsSync(stateFile), false);
|
|
4389
|
+
}
|
|
4390
|
+
finally {
|
|
4391
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
4392
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4393
|
+
}
|
|
4394
|
+
});
|
|
4395
|
+
it("keeps triage default-enabled when config omits promptRouting.triage.enabled", async () => {
|
|
4396
|
+
const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-home-"));
|
|
4397
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-cwd-"));
|
|
4398
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
4399
|
+
try {
|
|
4400
|
+
await writeJson(join(tmpHome, ".omx-config.json"), {
|
|
4401
|
+
promptRouting: {},
|
|
4402
|
+
});
|
|
4403
|
+
process.env.CODEX_HOME = tmpHome;
|
|
4404
|
+
resetTriageConfigCache();
|
|
4405
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4406
|
+
const result = await dispatchCodexNativeHook({
|
|
4407
|
+
hook_event_name: "UserPromptSubmit",
|
|
4408
|
+
cwd,
|
|
4409
|
+
session_id: "triage-defaulted-1",
|
|
4410
|
+
thread_id: "thread-triage-defaulted-1",
|
|
4411
|
+
turn_id: "turn-triage-defaulted-1",
|
|
4412
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4413
|
+
}, { cwd });
|
|
4414
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4415
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4416
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-defaulted-1", "prompt-routing-state.json");
|
|
4417
|
+
assert.equal(existsSync(stateFile), true);
|
|
4418
|
+
}
|
|
4419
|
+
finally {
|
|
4420
|
+
if (typeof previousCodexHome === "string")
|
|
4421
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
4422
|
+
else
|
|
4423
|
+
delete process.env.CODEX_HOME;
|
|
4424
|
+
resetTriageConfigCache();
|
|
4425
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
4426
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4427
|
+
}
|
|
4428
|
+
});
|
|
4429
|
+
it("does not suppress a short anchored follow-up that is a new request", async () => {
|
|
4430
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-short-new-request-"));
|
|
4431
|
+
const sessionId = "triage-short-new-request-1";
|
|
4432
|
+
try {
|
|
4433
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4434
|
+
await dispatchCodexNativeHook({
|
|
4435
|
+
hook_event_name: "UserPromptSubmit",
|
|
4436
|
+
cwd,
|
|
4437
|
+
session_id: sessionId,
|
|
4438
|
+
thread_id: "thread-short-new-request-1",
|
|
4439
|
+
turn_id: "turn-short-new-request-1",
|
|
4440
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4441
|
+
}, { cwd });
|
|
4442
|
+
const turn2 = await dispatchCodexNativeHook({
|
|
4443
|
+
hook_event_name: "UserPromptSubmit",
|
|
4444
|
+
cwd,
|
|
4445
|
+
session_id: sessionId,
|
|
4446
|
+
thread_id: "thread-short-new-request-1",
|
|
4447
|
+
turn_id: "turn-short-new-request-2",
|
|
4448
|
+
prompt: "fix typo in src/foo.ts",
|
|
4449
|
+
}, { cwd });
|
|
4450
|
+
const ctx2 = String(turn2.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4451
|
+
assert.match(ctx2, /narrow edit-shaped/);
|
|
4452
|
+
}
|
|
4453
|
+
finally {
|
|
4454
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4455
|
+
}
|
|
4456
|
+
});
|
|
4457
|
+
it("skips triage state persistence for malformed explicit session ids without writing root state", async () => {
|
|
4458
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-invalid-session-"));
|
|
4459
|
+
try {
|
|
4460
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4461
|
+
const result = await dispatchCodexNativeHook({
|
|
4462
|
+
hook_event_name: "UserPromptSubmit",
|
|
4463
|
+
cwd,
|
|
4464
|
+
session_id: "bad/session",
|
|
4465
|
+
thread_id: "thread-triage-invalid-session-1",
|
|
4466
|
+
turn_id: "turn-triage-invalid-session-1",
|
|
4467
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4468
|
+
}, { cwd });
|
|
4469
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
4470
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4471
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "prompt-routing-state.json")), false);
|
|
4472
|
+
}
|
|
4473
|
+
finally {
|
|
4474
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4475
|
+
}
|
|
4476
|
+
});
|
|
4477
|
+
});
|
|
3145
4478
|
//# sourceMappingURL=codex-native-hook.test.js.map
|