oh-my-codex 0.14.4 → 0.15.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/.agents/plugins/marketplace.json +20 -0
- package/Cargo.lock +6 -6
- package/Cargo.toml +2 -1
- package/README.md +40 -7
- package/crates/omx-explore/Cargo.toml +1 -0
- package/crates/omx-explore/src/main.rs +246 -24
- package/crates/omx-mux/Cargo.toml +1 -0
- package/crates/omx-runtime/Cargo.toml +1 -0
- package/crates/omx-runtime-core/Cargo.toml +1 -0
- package/crates/omx-runtime-core/src/dispatch.rs +2 -1
- package/crates/omx-runtime-core/src/engine.rs +2 -2
- package/crates/omx-runtime-core/src/mailbox.rs +2 -1
- package/crates/omx-sparkshell/Cargo.toml +1 -0
- package/crates/omx-sparkshell/src/codex_bridge.rs +1 -0
- package/crates/omx-sparkshell/src/prompt.rs +1 -0
- package/crates/omx-sparkshell/src/threshold.rs +1 -0
- package/dist/agents/__tests__/native-config.test.js +98 -4
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +4 -0
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +19 -3
- package/dist/agents/native-config.js.map +1 -1
- package/dist/agents/policy.d.ts +10 -0
- package/dist/agents/policy.d.ts.map +1 -0
- package/dist/agents/policy.js +61 -0
- package/dist/agents/policy.js.map +1 -0
- package/dist/catalog/__tests__/generator.test.js +2 -0
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts +2 -0
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts.map +1 -0
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +72 -0
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -0
- package/dist/catalog/installable.d.ts +5 -0
- package/dist/catalog/installable.d.ts.map +1 -0
- package/dist/catalog/installable.js +13 -0
- package/dist/catalog/installable.js.map +1 -0
- package/dist/catalog/skill-mirror.d.ts +20 -0
- package/dist/catalog/skill-mirror.d.ts.map +1 -0
- package/dist/catalog/skill-mirror.js +104 -0
- package/dist/catalog/skill-mirror.js.map +1 -0
- package/dist/cli/__tests__/ask.test.js +4 -1
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.d.ts +2 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.d.ts.map +1 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js +210 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-team.test.js +46 -1
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +225 -111
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +96 -1
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +26 -9
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +301 -7
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +358 -4
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/list.test.d.ts +2 -0
- package/dist/cli/__tests__/list.test.d.ts.map +1 -0
- package/dist/cli/__tests__/list.test.js +38 -0
- package/dist/cli/__tests__/list.test.js.map +1 -0
- package/dist/cli/__tests__/mcp-parity.test.js +86 -0
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-serve.test.d.ts +2 -0
- package/dist/cli/__tests__/mcp-serve.test.d.ts.map +1 -0
- package/dist/cli/__tests__/mcp-serve.test.js +38 -0
- package/dist/cli/__tests__/mcp-serve.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +58 -3
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +209 -5
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +146 -3
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-install-mode.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-install-mode.test.js +873 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -0
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +83 -22
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +120 -2
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +32 -13
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/sidecar.test.d.ts +2 -0
- package/dist/cli/__tests__/sidecar.test.d.ts.map +1 -0
- package/dist/cli/__tests__/sidecar.test.js +24 -0
- package/dist/cli/__tests__/sidecar.test.js.map +1 -0
- package/dist/cli/__tests__/team.test.js +90 -42
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/version-sync-contract.test.js +11 -0
- package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
- package/dist/cli/codex-home.d.ts +4 -5
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +9 -37
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +509 -278
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +13 -5
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +351 -60
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/list.d.ts +2 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +40 -0
- package/dist/cli/list.js.map +1 -0
- package/dist/cli/mcp-serve.d.ts +12 -0
- package/dist/cli/mcp-serve.d.ts.map +1 -0
- package/dist/cli/mcp-serve.js +63 -0
- package/dist/cli/mcp-serve.js.map +1 -0
- package/dist/cli/plugin-marketplace.d.ts +13 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -0
- package/dist/cli/plugin-marketplace.js +77 -0
- package/dist/cli/plugin-marketplace.js.map +1 -0
- package/dist/cli/question.d.ts.map +1 -1
- package/dist/cli/question.js +27 -2
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +20 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -0
- package/dist/cli/setup-preferences.js +71 -0
- package/dist/cli/setup-preferences.js.map +1 -0
- package/dist/cli/setup.d.ts +12 -3
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1080 -254
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts +1 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +22 -3
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +2 -8
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +100 -3
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +6 -5
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/generator-status-line-presets.test.d.ts +2 -0
- package/dist/config/__tests__/generator-status-line-presets.test.d.ts.map +1 -0
- package/dist/config/__tests__/generator-status-line-presets.test.js +203 -0
- package/dist/config/__tests__/generator-status-line-presets.test.js.map +1 -0
- package/dist/config/__tests__/models.test.js +35 -2
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2 -7
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/generator.d.ts +10 -0
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +259 -76
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +13 -3
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +25 -5
- package/dist/config/models.js.map +1 -1
- package/dist/config/omx-first-party-mcp.d.ts +18 -0
- package/dist/config/omx-first-party-mcp.d.ts.map +1 -0
- package/dist/config/omx-first-party-mcp.js +76 -0
- package/dist/config/omx-first-party-mcp.js.map +1 -0
- package/dist/document-refresh/__tests__/enforcer.test.d.ts +2 -0
- package/dist/document-refresh/__tests__/enforcer.test.d.ts.map +1 -0
- package/dist/document-refresh/__tests__/enforcer.test.js +128 -0
- package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -0
- package/dist/document-refresh/config.d.ts +9 -0
- package/dist/document-refresh/config.d.ts.map +1 -0
- package/dist/document-refresh/config.js +70 -0
- package/dist/document-refresh/config.js.map +1 -0
- package/dist/document-refresh/enforcer.d.ts +43 -0
- package/dist/document-refresh/enforcer.d.ts.map +1 -0
- package/dist/document-refresh/enforcer.js +329 -0
- package/dist/document-refresh/enforcer.js.map +1 -0
- package/dist/exec/followup.d.ts +44 -0
- package/dist/exec/followup.d.ts.map +1 -0
- package/dist/exec/followup.js +349 -0
- package/dist/exec/followup.js.map +1 -0
- package/dist/hooks/__tests__/codebase-map.test.js +63 -1
- package/dist/hooks/__tests__/codebase-map.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -7
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +89 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +84 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +23 -2
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +43 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +17 -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 +53 -0
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +4 -4
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +103 -0
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +27 -13
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +35 -0
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +70 -3
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js +5 -0
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +3 -2
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prompt-refactor-contract.test.js +22 -0
- package/dist/hooks/__tests__/prompt-refactor-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +28 -0
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/triage-heuristic.test.js +59 -4
- package/dist/hooks/__tests__/triage-heuristic.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts +2 -0
- package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/visual-ralph-skill.test.js +44 -0
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -0
- package/dist/hooks/codebase-map.d.ts.map +1 -1
- package/dist/hooks/codebase-map.js +83 -6
- package/dist/hooks/codebase-map.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +19 -14
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts +6 -0
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +89 -0
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/session.d.ts +2 -0
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +6 -0
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/triage-heuristic.d.ts +2 -2
- package/dist/hooks/triage-heuristic.d.ts.map +1 -1
- package/dist/hooks/triage-heuristic.js +97 -20
- package/dist/hooks/triage-heuristic.js.map +1 -1
- package/dist/hooks/triage-state.d.ts +1 -1
- package/dist/hooks/triage-state.d.ts.map +1 -1
- package/dist/hooks/triage-state.js +2 -1
- package/dist/hooks/triage-state.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +4 -4
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +4 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/types.test.js +27 -0
- package/dist/hud/__tests__/types.test.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +8 -0
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/types.d.ts +9 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js +3 -0
- package/dist/hud/types.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +45 -5
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +50 -7
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +67 -9
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +12 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +87 -29
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/state-server.d.ts +5 -11
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +16 -432
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/planning/__tests__/artifacts.test.js +152 -1
- package/dist/planning/__tests__/artifacts.test.js.map +1 -1
- package/dist/planning/artifacts.d.ts +9 -0
- package/dist/planning/artifacts.d.ts.map +1 -1
- package/dist/planning/artifacts.js +60 -1
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/__tests__/deep-interview.test.js +1 -1
- package/dist/question/__tests__/deep-interview.test.js.map +1 -1
- package/dist/question/__tests__/renderer.test.js +70 -0
- package/dist/question/__tests__/renderer.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js +49 -0
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/renderer.d.ts +14 -1
- package/dist/question/renderer.d.ts.map +1 -1
- package/dist/question/renderer.js +109 -10
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts +2 -0
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js +4 -0
- package/dist/question/state.js.map +1 -1
- package/dist/question/types.d.ts +2 -1
- package/dist/question/types.d.ts.map +1 -1
- package/dist/question/types.js.map +1 -1
- package/dist/question/ui.d.ts.map +1 -1
- package/dist/question/ui.js +28 -8
- package/dist/question/ui.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +143 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +21 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +54 -4
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1904 -230
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/generate-release-body.test.js +90 -1
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js +110 -2
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +7 -93
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/prompt-inventory.test.d.ts +2 -0
- package/dist/scripts/__tests__/prompt-inventory.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/prompt-inventory.test.js +56 -0
- package/dist/scripts/__tests__/prompt-inventory.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.d.ts +2 -0
- package/dist/scripts/__tests__/run-test-files.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js +62 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -0
- package/dist/scripts/__tests__/smoke-packed-install.test.js +13 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-agents.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-agents.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-agents.test.js +166 -0
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -0
- package/dist/scripts/codex-execution-surface.d.ts +16 -0
- package/dist/scripts/codex-execution-surface.d.ts.map +1 -0
- package/dist/scripts/codex-execution-surface.js +42 -0
- package/dist/scripts/codex-execution-surface.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +418 -76
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +8 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +133 -19
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-release-body.d.ts +1 -0
- package/dist/scripts/generate-release-body.d.ts.map +1 -1
- package/dist/scripts/generate-release-body.js +32 -3
- package/dist/scripts/generate-release-body.js.map +1 -1
- package/dist/scripts/hook-derived-watcher.js +46 -7
- package/dist/scripts/hook-derived-watcher.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +59 -9
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts +2 -0
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts.map +1 -0
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js +153 -0
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js.map +1 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +18 -1
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
- package/dist/scripts/notify-hook/process-runner.js +7 -3
- package/dist/scripts/notify-hook/process-runner.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +96 -11
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +3 -3
- package/dist/scripts/notify-hook/team-worker-posttooluse.d.ts +34 -0
- package/dist/scripts/notify-hook/team-worker-posttooluse.d.ts.map +1 -0
- package/dist/scripts/notify-hook/team-worker-posttooluse.js +434 -0
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -0
- package/dist/scripts/notify-hook/team-worker.d.ts +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +3 -43
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook.js +36 -5
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.d.ts +1 -5
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +3 -42
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/prompt-inventory.d.ts +29 -0
- package/dist/scripts/prompt-inventory.d.ts.map +1 -0
- package/dist/scripts/prompt-inventory.js +178 -0
- package/dist/scripts/prompt-inventory.js.map +1 -0
- package/dist/scripts/run-test-files.js +32 -2
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +3 -0
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +9 -1
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts +13 -0
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -0
- package/dist/scripts/sync-plugin-mirror.js +242 -0
- package/dist/scripts/sync-plugin-mirror.js.map +1 -0
- package/dist/scripts/verify-native-agents.d.ts +16 -0
- package/dist/scripts/verify-native-agents.d.ts.map +1 -0
- package/dist/scripts/verify-native-agents.js +188 -0
- package/dist/scripts/verify-native-agents.js.map +1 -0
- package/dist/session-history/search.d.ts.map +1 -1
- package/dist/session-history/search.js +7 -2
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/boundary.test.d.ts +2 -0
- package/dist/sidecar/__tests__/boundary.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/boundary.test.js +48 -0
- package/dist/sidecar/__tests__/boundary.test.js.map +1 -0
- package/dist/sidecar/__tests__/collector.test.d.ts +2 -0
- package/dist/sidecar/__tests__/collector.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/collector.test.js +162 -0
- package/dist/sidecar/__tests__/collector.test.js.map +1 -0
- package/dist/sidecar/__tests__/render.test.d.ts +2 -0
- package/dist/sidecar/__tests__/render.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/render.test.js +67 -0
- package/dist/sidecar/__tests__/render.test.js.map +1 -0
- package/dist/sidecar/__tests__/tmux.test.d.ts +2 -0
- package/dist/sidecar/__tests__/tmux.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/tmux.test.js +30 -0
- package/dist/sidecar/__tests__/tmux.test.js.map +1 -0
- package/dist/sidecar/__tests__/watch.test.d.ts +2 -0
- package/dist/sidecar/__tests__/watch.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/watch.test.js +42 -0
- package/dist/sidecar/__tests__/watch.test.js.map +1 -0
- package/dist/sidecar/collector.d.ts +4 -0
- package/dist/sidecar/collector.d.ts.map +1 -0
- package/dist/sidecar/collector.js +377 -0
- package/dist/sidecar/collector.js.map +1 -0
- package/dist/sidecar/index.d.ts +25 -0
- package/dist/sidecar/index.d.ts.map +1 -0
- package/dist/sidecar/index.js +165 -0
- package/dist/sidecar/index.js.map +1 -0
- package/dist/sidecar/render.d.ts +3 -0
- package/dist/sidecar/render.d.ts.map +1 -0
- package/dist/sidecar/render.js +72 -0
- package/dist/sidecar/render.js.map +1 -0
- package/dist/sidecar/tmux.d.ts +13 -0
- package/dist/sidecar/tmux.d.ts.map +1 -0
- package/dist/sidecar/tmux.js +44 -0
- package/dist/sidecar/tmux.js.map +1 -0
- package/dist/sidecar/types.d.ts +125 -0
- package/dist/sidecar/types.d.ts.map +1 -0
- package/dist/sidecar/types.js +2 -0
- package/dist/sidecar/types.js.map +1 -0
- package/dist/state/__tests__/operations.test.js +50 -22
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +19 -7
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/commit-hygiene.test.d.ts +2 -0
- package/dist/team/__tests__/commit-hygiene.test.d.ts.map +1 -0
- package/dist/team/__tests__/commit-hygiene.test.js +93 -0
- package/dist/team/__tests__/commit-hygiene.test.js.map +1 -0
- package/dist/team/__tests__/delegation-policy.test.d.ts +2 -0
- package/dist/team/__tests__/delegation-policy.test.d.ts.map +1 -0
- package/dist/team/__tests__/delegation-policy.test.js +69 -0
- package/dist/team/__tests__/delegation-policy.test.js.map +1 -0
- package/dist/team/__tests__/events.test.js +54 -4
- package/dist/team/__tests__/events.test.js.map +1 -1
- package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts +2 -0
- package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts.map +1 -0
- package/dist/team/__tests__/hook-primary-e2e-contract.test.js +78 -0
- package/dist/team/__tests__/hook-primary-e2e-contract.test.js.map +1 -0
- package/dist/team/__tests__/model-contract.test.js +18 -2
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/repo-aware-decomposition.test.d.ts +2 -0
- package/dist/team/__tests__/repo-aware-decomposition.test.d.ts.map +1 -0
- package/dist/team/__tests__/repo-aware-decomposition.test.js +95 -0
- package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -0
- package/dist/team/__tests__/runtime.test.js +677 -17
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +177 -1
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +110 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +301 -2
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +94 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/commit-hygiene.d.ts +22 -3
- package/dist/team/commit-hygiene.d.ts.map +1 -1
- package/dist/team/commit-hygiene.js +134 -2
- package/dist/team/commit-hygiene.js.map +1 -1
- package/dist/team/contracts.d.ts +1 -1
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/contracts.js +2 -0
- package/dist/team/contracts.js.map +1 -1
- package/dist/team/dag-schema.d.ts +38 -0
- package/dist/team/dag-schema.d.ts.map +1 -0
- package/dist/team/dag-schema.js +221 -0
- package/dist/team/dag-schema.js.map +1 -0
- package/dist/team/delegation-policy.d.ts +3 -0
- package/dist/team/delegation-policy.d.ts.map +1 -0
- package/dist/team/delegation-policy.js +82 -0
- package/dist/team/delegation-policy.js.map +1 -0
- package/dist/team/model-contract.d.ts +3 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +44 -5
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/repo-aware-decomposition.d.ts +60 -0
- package/dist/team/repo-aware-decomposition.d.ts.map +1 -0
- package/dist/team/repo-aware-decomposition.js +229 -0
- package/dist/team/repo-aware-decomposition.js.map +1 -0
- package/dist/team/runtime.d.ts +34 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +191 -50
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +33 -0
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +23 -1
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +35 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +281 -1
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +27 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +6 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +83 -6
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +3 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +77 -4
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +8 -0
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +42 -9
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts +3 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +25 -0
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +2 -3
- package/dist/utils/agents-model-table.js.map +1 -1
- package/package.json +14 -7
- package/plugins/oh-my-codex/.app.json +3 -0
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +30 -0
- package/plugins/oh-my-codex/.mcp.json +44 -0
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +114 -0
- package/plugins/oh-my-codex/skills/analyze/SKILL.md +148 -0
- package/plugins/oh-my-codex/skills/ask-claude/SKILL.md +61 -0
- package/plugins/oh-my-codex/skills/ask-gemini/SKILL.md +61 -0
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +231 -0
- package/plugins/oh-my-codex/skills/autoresearch/SKILL.md +68 -0
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +399 -0
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +290 -0
- package/plugins/oh-my-codex/skills/configure-notifications/SKILL.md +287 -0
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +464 -0
- package/plugins/oh-my-codex/skills/doctor/SKILL.md +239 -0
- package/plugins/oh-my-codex/skills/help/SKILL.md +202 -0
- package/plugins/oh-my-codex/skills/hud/SKILL.md +98 -0
- package/plugins/oh-my-codex/skills/note/SKILL.md +62 -0
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +135 -0
- package/plugins/oh-my-codex/skills/pipeline/SKILL.md +86 -0
- package/plugins/oh-my-codex/skills/plan/SKILL.md +276 -0
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +269 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +162 -0
- package/plugins/oh-my-codex/skills/security-review/SKILL.md +300 -0
- package/plugins/oh-my-codex/skills/skill/SKILL.md +835 -0
- package/plugins/oh-my-codex/skills/team/SKILL.md +510 -0
- package/plugins/oh-my-codex/skills/trace/SKILL.md +33 -0
- package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +143 -0
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +175 -0
- package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +153 -0
- package/plugins/oh-my-codex/skills/visual-verdict/SKILL.md +76 -0
- package/plugins/oh-my-codex/skills/wiki/SKILL.md +57 -0
- package/plugins/oh-my-codex/skills/worker/SKILL.md +106 -0
- package/prompts/critic.md +35 -83
- package/prompts/executor.md +32 -109
- package/prompts/explore-harness.md +1 -1
- package/prompts/explore.md +37 -90
- package/prompts/planner.md +28 -58
- package/prompts/researcher.md +46 -78
- package/prompts/verifier.md +23 -31
- package/skills/autopilot/SKILL.md +1 -4
- package/skills/deep-interview/SKILL.md +8 -5
- package/skills/doctor/SKILL.md +45 -17
- package/skills/help/SKILL.md +2 -2
- package/skills/omx-setup/SKILL.md +52 -9
- package/skills/plan/SKILL.md +1 -4
- package/skills/ralph/SKILL.md +3 -5
- package/skills/ralplan/SKILL.md +2 -6
- package/skills/team/SKILL.md +2 -5
- package/skills/ultraqa/SKILL.md +1 -4
- package/skills/ultrawork/SKILL.md +2 -3
- package/skills/visual-ralph/SKILL.md +153 -0
- package/skills/web-clone/SKILL.md +12 -354
- package/src/scripts/__tests__/codex-native-hook.test.ts +2306 -224
- package/src/scripts/__tests__/generate-release-body.test.ts +109 -0
- package/src/scripts/__tests__/hook-derived-watcher.test.ts +132 -2
- package/src/scripts/__tests__/postinstall.test.ts +7 -104
- package/src/scripts/__tests__/prompt-inventory.test.ts +64 -0
- package/src/scripts/__tests__/run-test-files.test.ts +72 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +15 -0
- package/src/scripts/__tests__/verify-native-agents.test.ts +204 -0
- package/src/scripts/codex-execution-surface.ts +73 -0
- package/src/scripts/codex-native-hook.ts +579 -82
- package/src/scripts/codex-native-pre-post.ts +157 -19
- package/src/scripts/generate-release-body.ts +34 -2
- package/src/scripts/hook-derived-watcher.ts +51 -8
- package/src/scripts/notify-fallback-watcher.ts +65 -9
- package/src/scripts/notify-hook/__tests__/team-worker-posttooluse.test.ts +180 -0
- package/src/scripts/notify-hook/managed-tmux.ts +22 -4
- package/src/scripts/notify-hook/process-runner.ts +7 -3
- package/src/scripts/notify-hook/team-dispatch.ts +103 -11
- package/src/scripts/notify-hook/team-tmux-guard.ts +3 -3
- package/src/scripts/notify-hook/team-worker-posttooluse.ts +536 -0
- package/src/scripts/notify-hook/team-worker.ts +4 -48
- package/src/scripts/notify-hook.ts +36 -5
- package/src/scripts/postinstall.ts +4 -58
- package/src/scripts/prompt-inventory.ts +218 -0
- package/src/scripts/run-test-files.ts +41 -2
- package/src/scripts/smoke-packed-install.ts +10 -1
- package/src/scripts/sync-plugin-mirror.ts +360 -0
- package/src/scripts/verify-native-agents.ts +245 -0
- package/templates/AGENTS.md +26 -91
- package/templates/catalog-manifest.json +16 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
9
9
|
import { buildManagedCodexHooksConfig } from "../../config/codex-hooks.js";
|
|
10
|
+
import { DOCUMENT_REFRESH_EXEMPTION_PREFIX } from "../../document-refresh/enforcer.js";
|
|
10
11
|
import {
|
|
11
12
|
initTeamState,
|
|
12
13
|
readTeamLeaderAttention,
|
|
@@ -21,12 +22,51 @@ import {
|
|
|
21
22
|
} from "../codex-native-hook.js";
|
|
22
23
|
import { writeSessionStart } from "../../hooks/session.js";
|
|
23
24
|
import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
25
|
+
import { executeStateOperation } from "../../state/operations.js";
|
|
26
|
+
|
|
27
|
+
function nativeHookScriptPath(): string {
|
|
28
|
+
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseSingleJsonStdout(stdout: string): Record<string, unknown> {
|
|
32
|
+
const trimmed = stdout.trim();
|
|
33
|
+
assert.notEqual(trimmed, "");
|
|
34
|
+
assert.equal(trimmed.split("\n").length, 1);
|
|
35
|
+
return JSON.parse(trimmed) as Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runNativeHookCli(
|
|
39
|
+
payload: Record<string, unknown> | string,
|
|
40
|
+
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
|
41
|
+
): string {
|
|
42
|
+
return execFileSync(
|
|
43
|
+
process.execPath,
|
|
44
|
+
[nativeHookScriptPath()],
|
|
45
|
+
{
|
|
46
|
+
cwd: options.cwd ?? process.cwd(),
|
|
47
|
+
input: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
48
|
+
encoding: "utf-8",
|
|
49
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
50
|
+
env: options.env ?? process.env,
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
24
54
|
|
|
25
55
|
async function writeJson(path: string, value: unknown): Promise<void> {
|
|
26
56
|
await mkdir(dirname(path), { recursive: true }).catch(() => {});
|
|
27
57
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
28
58
|
}
|
|
29
59
|
|
|
60
|
+
async function writeActiveAutopilotSession(cwd: string, sessionId: string): Promise<void> {
|
|
61
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), {
|
|
62
|
+
session_id: sessionId,
|
|
63
|
+
});
|
|
64
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
65
|
+
active: true,
|
|
66
|
+
current_phase: "execution",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
async function writeHookCounterPlugin(cwd: string): Promise<string> {
|
|
31
71
|
const markerPath = join(cwd, ".omx", "stop-hook-counter.json");
|
|
32
72
|
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
@@ -101,6 +141,9 @@ const TEAM_ENV_KEYS = [
|
|
|
101
141
|
"OMX_TEAM_STATE_ROOT",
|
|
102
142
|
"OMX_TEAM_LEADER_CWD",
|
|
103
143
|
"OMX_SESSION_ID",
|
|
144
|
+
"OMX_QUESTION_RETURN_PANE",
|
|
145
|
+
"OMX_LEADER_PANE_ID",
|
|
146
|
+
"TMUX",
|
|
104
147
|
"TMUX_PANE",
|
|
105
148
|
] as const;
|
|
106
149
|
|
|
@@ -150,6 +193,7 @@ describe("codex native hook config", () => {
|
|
|
150
193
|
String(preToolUse.hooks?.[0]?.command || ""),
|
|
151
194
|
/codex-native-hook\.js"?$/,
|
|
152
195
|
);
|
|
196
|
+
assert.equal(preToolUse.hooks?.[0]?.statusMessage, undefined);
|
|
153
197
|
|
|
154
198
|
const postToolUse = config.hooks.PostToolUse[0] as {
|
|
155
199
|
matcher?: string;
|
|
@@ -160,7 +204,18 @@ describe("codex native hook config", () => {
|
|
|
160
204
|
String(postToolUse.hooks?.[0]?.command || ""),
|
|
161
205
|
/codex-native-hook\.js"?$/,
|
|
162
206
|
);
|
|
163
|
-
assert.equal(postToolUse.hooks?.[0]?.statusMessage,
|
|
207
|
+
assert.equal(postToolUse.hooks?.[0]?.statusMessage, undefined);
|
|
208
|
+
|
|
209
|
+
const userPromptSubmit = config.hooks.UserPromptSubmit[0] as {
|
|
210
|
+
matcher?: string;
|
|
211
|
+
hooks?: Array<Record<string, unknown>>;
|
|
212
|
+
};
|
|
213
|
+
assert.equal(userPromptSubmit.matcher, undefined);
|
|
214
|
+
assert.match(
|
|
215
|
+
String(userPromptSubmit.hooks?.[0]?.command || ""),
|
|
216
|
+
/codex-native-hook\.js"?$/,
|
|
217
|
+
);
|
|
218
|
+
assert.equal(userPromptSubmit.hooks?.[0]?.statusMessage, undefined);
|
|
164
219
|
|
|
165
220
|
const stop = config.hooks.Stop[0] as {
|
|
166
221
|
hooks?: Array<Record<string, unknown>>;
|
|
@@ -190,18 +245,9 @@ describe("codex native hook dispatch", () => {
|
|
|
190
245
|
});
|
|
191
246
|
|
|
192
247
|
it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
|
|
193
|
-
const stdout =
|
|
194
|
-
process.execPath,
|
|
195
|
-
[join(process.cwd(), "dist", "scripts", "codex-native-hook.js")],
|
|
196
|
-
{
|
|
197
|
-
cwd: process.cwd(),
|
|
198
|
-
input: "{",
|
|
199
|
-
encoding: "utf-8",
|
|
200
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
201
|
-
},
|
|
202
|
-
);
|
|
248
|
+
const stdout = runNativeHookCli("{");
|
|
203
249
|
|
|
204
|
-
const output =
|
|
250
|
+
const output = parseSingleJsonStdout(stdout) as {
|
|
205
251
|
decision?: string;
|
|
206
252
|
reason?: string;
|
|
207
253
|
hookSpecificOutput?: { hookEventName?: string; additionalContext?: string };
|
|
@@ -219,6 +265,129 @@ describe("codex native hook dispatch", () => {
|
|
|
219
265
|
);
|
|
220
266
|
});
|
|
221
267
|
|
|
268
|
+
it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
|
|
269
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
|
|
270
|
+
try {
|
|
271
|
+
const stdout = runNativeHookCli({
|
|
272
|
+
hook_event_name: "Stop",
|
|
273
|
+
cwd,
|
|
274
|
+
session_id: "sess-cli-stop-noop-json",
|
|
275
|
+
thread_id: "thread-cli-stop-noop-json",
|
|
276
|
+
turn_id: "turn-cli-stop-noop-json",
|
|
277
|
+
}, { cwd });
|
|
278
|
+
const output = parseSingleJsonStdout(stdout);
|
|
279
|
+
|
|
280
|
+
assert.deepEqual(output, {});
|
|
281
|
+
} finally {
|
|
282
|
+
await rm(cwd, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does not crash Stop hook dispatch when the exec follow-up queue is malformed", async () => {
|
|
287
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-stop-exec-followup-corrupt-"));
|
|
288
|
+
try {
|
|
289
|
+
const session = await writeSessionStart(cwd, "sess-exec-followup-corrupt");
|
|
290
|
+
const queuePath = join(cwd, ".omx", "state", "sessions", session.session_id, "exec-followups.json");
|
|
291
|
+
await mkdir(dirname(queuePath), { recursive: true });
|
|
292
|
+
await writeFile(queuePath, '{"version":1,"records":[', "utf-8");
|
|
293
|
+
|
|
294
|
+
const result = await dispatchCodexNativeHook({
|
|
295
|
+
hook_event_name: "Stop",
|
|
296
|
+
cwd,
|
|
297
|
+
session_id: session.session_id,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
assert.equal(result.hookEventName, "Stop");
|
|
301
|
+
assert.equal(result.outputJson, null);
|
|
302
|
+
const queueDirEntries = await readdir(dirname(queuePath));
|
|
303
|
+
assert.ok(queueDirEntries.some((entry) => entry.startsWith("exec-followups.json.corrupt-")));
|
|
304
|
+
const auditPath = join(cwd, ".omx", "logs", `exec-followups-${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
305
|
+
assert.match(await readFile(auditPath, "utf-8"), /exec_followup_queue_corrupt_recovered/);
|
|
306
|
+
} finally {
|
|
307
|
+
await rm(cwd, { recursive: true, force: true });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("emits exactly one parseable JSON object for active Stop CLI continuation", async () => {
|
|
312
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-json-"));
|
|
313
|
+
try {
|
|
314
|
+
await writeActiveAutopilotSession(cwd, "sess-cli-stop-json");
|
|
315
|
+
|
|
316
|
+
const stdout = runNativeHookCli({
|
|
317
|
+
hook_event_name: "Stop",
|
|
318
|
+
cwd,
|
|
319
|
+
session_id: "sess-cli-stop-json",
|
|
320
|
+
thread_id: "thread-cli-stop-json",
|
|
321
|
+
turn_id: "turn-cli-stop-json",
|
|
322
|
+
}, { cwd });
|
|
323
|
+
const output = parseSingleJsonStdout(stdout);
|
|
324
|
+
|
|
325
|
+
assert.equal(output.decision, "block");
|
|
326
|
+
assert.equal(output.stopReason, "autopilot_execution");
|
|
327
|
+
} finally {
|
|
328
|
+
await rm(cwd, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("keeps noisy Stop hook plugin stdout out of native Stop CLI stdout", async () => {
|
|
333
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noisy-plugin-"));
|
|
334
|
+
try {
|
|
335
|
+
await writeActiveAutopilotSession(cwd, "sess-cli-stop-noisy-plugin");
|
|
336
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
337
|
+
await writeFile(
|
|
338
|
+
join(cwd, ".omx", "hooks", "noisy.mjs"),
|
|
339
|
+
`export async function onHookEvent(event) {
|
|
340
|
+
if (event.event === "stop") console.log("PLUGIN_NOISE");
|
|
341
|
+
}
|
|
342
|
+
`,
|
|
343
|
+
"utf-8",
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const stdout = runNativeHookCli({
|
|
347
|
+
hook_event_name: "Stop",
|
|
348
|
+
cwd,
|
|
349
|
+
session_id: "sess-cli-stop-noisy-plugin",
|
|
350
|
+
thread_id: "thread-cli-stop-noisy-plugin",
|
|
351
|
+
turn_id: "turn-cli-stop-noisy-plugin",
|
|
352
|
+
}, { cwd });
|
|
353
|
+
assert.doesNotMatch(stdout, /PLUGIN_NOISE/);
|
|
354
|
+
const output = parseSingleJsonStdout(stdout);
|
|
355
|
+
|
|
356
|
+
assert.equal(output.decision, "block");
|
|
357
|
+
assert.equal(output.stopReason, "autopilot_execution");
|
|
358
|
+
} finally {
|
|
359
|
+
await rm(cwd, { recursive: true, force: true });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("emits deterministic Stop JSON stdout when Stop dispatch fails", async () => {
|
|
364
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-dispatch-failure-"));
|
|
365
|
+
try {
|
|
366
|
+
const stdout = runNativeHookCli({
|
|
367
|
+
hook_event_name: "Stop",
|
|
368
|
+
cwd,
|
|
369
|
+
session_id: "sess-cli-stop-dispatch-failure",
|
|
370
|
+
thread_id: "thread-cli-stop-dispatch-failure",
|
|
371
|
+
turn_id: "turn-cli-stop-dispatch-failure",
|
|
372
|
+
}, {
|
|
373
|
+
cwd,
|
|
374
|
+
env: {
|
|
375
|
+
...process.env,
|
|
376
|
+
NODE_ENV: "test",
|
|
377
|
+
OMX_NATIVE_HOOK_TEST_THROW_STOP_DISPATCH: "1",
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
const output = parseSingleJsonStdout(stdout);
|
|
381
|
+
|
|
382
|
+
assert.equal(output.decision, "block");
|
|
383
|
+
assert.equal(output.stopReason, "native_stop_dispatch_failure");
|
|
384
|
+
assert.match(String(output.reason), /failed before normal continuation handling/);
|
|
385
|
+
assert.match(String(output.systemMessage), /test-induced Stop dispatch failure/);
|
|
386
|
+
} finally {
|
|
387
|
+
await rm(cwd, { recursive: true, force: true });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
222
391
|
it("maps Codex events onto OMX logical surfaces", () => {
|
|
223
392
|
assert.equal(mapCodexHookEventToOmxEvent("SessionStart"), "session-start");
|
|
224
393
|
assert.equal(mapCodexHookEventToOmxEvent("UserPromptSubmit"), "keyword-detector");
|
|
@@ -227,7 +396,7 @@ describe("codex native hook dispatch", () => {
|
|
|
227
396
|
assert.equal(mapCodexHookEventToOmxEvent("Stop"), "stop");
|
|
228
397
|
});
|
|
229
398
|
|
|
230
|
-
it("writes SessionStart state against the long-lived session owner pid and
|
|
399
|
+
it("writes SessionStart state against the long-lived session owner pid and injects environment context", async () => {
|
|
231
400
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-start-"));
|
|
232
401
|
try {
|
|
233
402
|
const result = await dispatchCodexNativeHook(
|
|
@@ -243,7 +412,13 @@ describe("codex native hook dispatch", () => {
|
|
|
243
412
|
);
|
|
244
413
|
|
|
245
414
|
assert.equal(result.omxEventName, "session-start");
|
|
246
|
-
|
|
415
|
+
const additionalContext = String(
|
|
416
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
417
|
+
);
|
|
418
|
+
assert.match(additionalContext, /\[Execution environment\]/);
|
|
419
|
+
assert.match(additionalContext, /native-hook \/ Codex App outside tmux/);
|
|
420
|
+
assert.match(additionalContext, /omx team, omx hud, and omx quest(?:ion) need an attached tmux OMX CLI shell|omx team and omx hud need an attached tmux OMX CLI shell/);
|
|
421
|
+
assert.match(additionalContext, /not available from this outside-tmux surface/);
|
|
247
422
|
const sessionState = JSON.parse(
|
|
248
423
|
await readFile(join(cwd, ".omx", "state", "session.json"), "utf-8"),
|
|
249
424
|
) as { session_id?: string; native_session_id?: string; pid?: number };
|
|
@@ -309,6 +484,221 @@ describe("codex native hook dispatch", () => {
|
|
|
309
484
|
}
|
|
310
485
|
});
|
|
311
486
|
|
|
487
|
+
it("keeps subagent SessionStart from replacing the canonical leader session", async () => {
|
|
488
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-"));
|
|
489
|
+
try {
|
|
490
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
491
|
+
const canonicalSessionId = "omx-leader-session";
|
|
492
|
+
const leaderNativeSessionId = "codex-leader-thread";
|
|
493
|
+
const childNativeSessionId = "codex-child-thread";
|
|
494
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
495
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
496
|
+
nativeSessionId: leaderNativeSessionId,
|
|
497
|
+
});
|
|
498
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
499
|
+
active: true,
|
|
500
|
+
mode: "ralph",
|
|
501
|
+
current_phase: "executing",
|
|
502
|
+
iteration: 1,
|
|
503
|
+
max_iterations: 5,
|
|
504
|
+
});
|
|
505
|
+
const transcriptPath = join(cwd, "subagent-rollout.jsonl");
|
|
506
|
+
await writeFile(
|
|
507
|
+
transcriptPath,
|
|
508
|
+
`${JSON.stringify({
|
|
509
|
+
type: "session_meta",
|
|
510
|
+
payload: {
|
|
511
|
+
id: childNativeSessionId,
|
|
512
|
+
source: {
|
|
513
|
+
subagent: {
|
|
514
|
+
thread_spawn: {
|
|
515
|
+
parent_thread_id: leaderNativeSessionId,
|
|
516
|
+
depth: 1,
|
|
517
|
+
agent_nickname: "Hegel",
|
|
518
|
+
agent_role: "critic",
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
agent_nickname: "Hegel",
|
|
523
|
+
agent_role: "critic",
|
|
524
|
+
},
|
|
525
|
+
})}\n`,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const result = await dispatchCodexNativeHook(
|
|
529
|
+
{
|
|
530
|
+
hook_event_name: "SessionStart",
|
|
531
|
+
cwd,
|
|
532
|
+
session_id: childNativeSessionId,
|
|
533
|
+
transcript_path: transcriptPath,
|
|
534
|
+
},
|
|
535
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const sessionState = JSON.parse(
|
|
539
|
+
await readFile(join(stateDir, "session.json"), "utf-8"),
|
|
540
|
+
) as { session_id?: string; native_session_id?: string };
|
|
541
|
+
assert.equal(sessionState.session_id, canonicalSessionId);
|
|
542
|
+
assert.equal(sessionState.native_session_id, leaderNativeSessionId);
|
|
543
|
+
assert.equal(
|
|
544
|
+
existsSync(join(stateDir, "sessions", childNativeSessionId, "ralph-state.json")),
|
|
545
|
+
false,
|
|
546
|
+
);
|
|
547
|
+
assert.ok(result.outputJson);
|
|
548
|
+
|
|
549
|
+
const leaderRalph = JSON.parse(
|
|
550
|
+
await readFile(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), "utf-8"),
|
|
551
|
+
) as { active?: boolean; current_phase?: string };
|
|
552
|
+
assert.equal(leaderRalph.active, true);
|
|
553
|
+
assert.equal(leaderRalph.current_phase, "executing");
|
|
554
|
+
|
|
555
|
+
const tracking = JSON.parse(
|
|
556
|
+
await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
|
|
557
|
+
) as {
|
|
558
|
+
sessions?: Record<string, {
|
|
559
|
+
leader_thread_id?: string;
|
|
560
|
+
threads?: Record<string, { kind?: string; mode?: string }>;
|
|
561
|
+
}>;
|
|
562
|
+
};
|
|
563
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, leaderNativeSessionId);
|
|
564
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
|
|
565
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
|
|
566
|
+
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
|
|
567
|
+
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
|
|
568
|
+
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
|
|
569
|
+
} finally {
|
|
570
|
+
await rm(cwd, { recursive: true, force: true });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
|
|
575
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
|
|
576
|
+
try {
|
|
577
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
578
|
+
const canonicalSessionId = "omx-leader-session-a";
|
|
579
|
+
const leaderNativeSessionId = "codex-leader-thread-a";
|
|
580
|
+
const unrelatedParentNativeSessionId = "codex-leader-thread-b";
|
|
581
|
+
const childNativeSessionId = "codex-child-thread-b";
|
|
582
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
583
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
584
|
+
nativeSessionId: leaderNativeSessionId,
|
|
585
|
+
});
|
|
586
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
587
|
+
active: true,
|
|
588
|
+
mode: "ralph",
|
|
589
|
+
current_phase: "executing",
|
|
590
|
+
iteration: 1,
|
|
591
|
+
max_iterations: 5,
|
|
592
|
+
});
|
|
593
|
+
const transcriptPath = join(cwd, "unrelated-subagent-rollout.jsonl");
|
|
594
|
+
await writeFile(
|
|
595
|
+
transcriptPath,
|
|
596
|
+
`${JSON.stringify({
|
|
597
|
+
type: "session_meta",
|
|
598
|
+
payload: {
|
|
599
|
+
id: childNativeSessionId,
|
|
600
|
+
source: {
|
|
601
|
+
subagent: {
|
|
602
|
+
thread_spawn: {
|
|
603
|
+
parent_thread_id: unrelatedParentNativeSessionId,
|
|
604
|
+
depth: 1,
|
|
605
|
+
agent_nickname: "Spinoza",
|
|
606
|
+
agent_role: "critic",
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
agent_nickname: "Spinoza",
|
|
611
|
+
agent_role: "critic",
|
|
612
|
+
},
|
|
613
|
+
})}\n`,
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const result = await dispatchCodexNativeHook(
|
|
617
|
+
{
|
|
618
|
+
hook_event_name: "SessionStart",
|
|
619
|
+
cwd,
|
|
620
|
+
session_id: childNativeSessionId,
|
|
621
|
+
transcript_path: transcriptPath,
|
|
622
|
+
},
|
|
623
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const sessionState = JSON.parse(
|
|
627
|
+
await readFile(join(stateDir, "session.json"), "utf-8"),
|
|
628
|
+
) as { session_id?: string; native_session_id?: string };
|
|
629
|
+
assert.equal(sessionState.session_id, canonicalSessionId);
|
|
630
|
+
assert.equal(sessionState.native_session_id, leaderNativeSessionId);
|
|
631
|
+
assert.equal(existsSync(join(stateDir, "subagent-tracking.json")), false);
|
|
632
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId)), false);
|
|
633
|
+
assert.equal(result.outputJson, null);
|
|
634
|
+
|
|
635
|
+
const leaderRalph = JSON.parse(
|
|
636
|
+
await readFile(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), "utf-8"),
|
|
637
|
+
) as { active?: boolean; current_phase?: string };
|
|
638
|
+
assert.equal(leaderRalph.active, true);
|
|
639
|
+
assert.equal(leaderRalph.current_phase, "executing");
|
|
640
|
+
} finally {
|
|
641
|
+
await rm(cwd, { recursive: true, force: true });
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("describes attached tmux runtime in SessionStart context when TMUX is present", async () => {
|
|
646
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-start-tmux-"));
|
|
647
|
+
process.env.TMUX = "/tmp/tmux-attached";
|
|
648
|
+
process.env.TMUX_PANE = "%11";
|
|
649
|
+
try {
|
|
650
|
+
const result = await dispatchCodexNativeHook(
|
|
651
|
+
{
|
|
652
|
+
hook_event_name: "SessionStart",
|
|
653
|
+
cwd,
|
|
654
|
+
session_id: "sess-start-tmux-1",
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
cwd,
|
|
658
|
+
sessionOwnerPid: process.pid,
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
const additionalContext = String(
|
|
663
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
664
|
+
);
|
|
665
|
+
assert.match(additionalContext, /\[Execution environment\]/);
|
|
666
|
+
assert.match(additionalContext, /attached tmux runtime/);
|
|
667
|
+
assert.match(additionalContext, /omx team, omx hud, and omx quest(?:ion) are directly usable in this session/);
|
|
668
|
+
assert.match(additionalContext, /visible renderer available from the current pane/);
|
|
669
|
+
} finally {
|
|
670
|
+
await rm(cwd, { recursive: true, force: true });
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("describes direct CLI outside tmux in SessionStart context when the launch source is cli", async () => {
|
|
675
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-start-cli-"));
|
|
676
|
+
try {
|
|
677
|
+
const result = await dispatchCodexNativeHook(
|
|
678
|
+
{
|
|
679
|
+
hook_event_name: "SessionStart",
|
|
680
|
+
cwd,
|
|
681
|
+
session_id: "sess-start-cli-1",
|
|
682
|
+
source: "cli",
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
cwd,
|
|
686
|
+
sessionOwnerPid: process.pid,
|
|
687
|
+
},
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const additionalContext = String(
|
|
691
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
692
|
+
);
|
|
693
|
+
assert.match(additionalContext, /\[Execution environment\]/);
|
|
694
|
+
assert.match(additionalContext, /direct CLI outside tmux/);
|
|
695
|
+
assert.doesNotMatch(additionalContext, /native-hook \/ Codex App outside tmux/);
|
|
696
|
+
assert.match(additionalContext, /omx team, omx hud, and omx quest(?:ion) need an attached tmux OMX CLI shell|omx team and omx hud need an attached tmux OMX CLI shell/);
|
|
697
|
+
} finally {
|
|
698
|
+
await rm(cwd, { recursive: true, force: true });
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
312
702
|
it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
|
|
313
703
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
|
|
314
704
|
try {
|
|
@@ -540,6 +930,8 @@ describe("codex native hook dispatch", () => {
|
|
|
540
930
|
const additionalContext = String(
|
|
541
931
|
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
542
932
|
);
|
|
933
|
+
assert.match(additionalContext, /\[Execution environment\]/);
|
|
934
|
+
assert.match(additionalContext, /native-hook \/ Codex App outside tmux/);
|
|
543
935
|
assert.match(additionalContext, /\[Priority notes\]/);
|
|
544
936
|
assert.match(additionalContext, /Preserve durable project guidance/);
|
|
545
937
|
assert.doesNotMatch(additionalContext, /stale UI rework context snapshot/);
|
|
@@ -607,6 +999,35 @@ describe("codex native hook dispatch", () => {
|
|
|
607
999
|
}
|
|
608
1000
|
});
|
|
609
1001
|
|
|
1002
|
+
it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
|
|
1003
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
|
|
1004
|
+
try {
|
|
1005
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1006
|
+
const result = await dispatchCodexNativeHook(
|
|
1007
|
+
{
|
|
1008
|
+
hook_event_name: "UserPromptSubmit",
|
|
1009
|
+
cwd,
|
|
1010
|
+
session_id: "sess-plugin-1",
|
|
1011
|
+
thread_id: "thread-plugin-1",
|
|
1012
|
+
turn_id: "turn-plugin-1",
|
|
1013
|
+
prompt: "$oh-my-codex:ralplan implement issue #1307",
|
|
1014
|
+
},
|
|
1015
|
+
{ cwd },
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1019
|
+
assert.equal(result.skillState?.skill, "ralplan");
|
|
1020
|
+
const message = String(
|
|
1021
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
1022
|
+
);
|
|
1023
|
+
assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
|
|
1024
|
+
assert.match(message, /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
|
|
1025
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
|
|
1026
|
+
} finally {
|
|
1027
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
610
1031
|
it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
|
|
611
1032
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
|
|
612
1033
|
try {
|
|
@@ -792,6 +1213,35 @@ describe("codex native hook dispatch", () => {
|
|
|
792
1213
|
}
|
|
793
1214
|
});
|
|
794
1215
|
|
|
1216
|
+
it("clarifies that plugin-prefixed prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
|
|
1217
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-ralph-routing-"));
|
|
1218
|
+
try {
|
|
1219
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1220
|
+
const result = await dispatchCodexNativeHook(
|
|
1221
|
+
{
|
|
1222
|
+
hook_event_name: "UserPromptSubmit",
|
|
1223
|
+
cwd,
|
|
1224
|
+
session_id: "sess-plugin-ralph-msg",
|
|
1225
|
+
thread_id: "thread-plugin-ralph-msg",
|
|
1226
|
+
turn_id: "turn-plugin-ralph-msg",
|
|
1227
|
+
prompt: "$oh-my-codex:ralph continue verification",
|
|
1228
|
+
},
|
|
1229
|
+
{ cwd },
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1233
|
+
assert.equal(result.skillState?.skill, "ralph");
|
|
1234
|
+
const message = String(
|
|
1235
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
1236
|
+
);
|
|
1237
|
+
assert.match(message, /\$oh-my-codex:ralph" -> ralph/);
|
|
1238
|
+
assert.match(message, /skill: ralph activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-ralph-msg\/ralph-state\.json; write subsequent updates via omx_state MCP\./);
|
|
1239
|
+
assert.match(message, /Prompt-side `\$ralph` activation seeds Ralph workflow state only; it does not invoke `omx ralph`\./);
|
|
1240
|
+
} finally {
|
|
1241
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
|
|
795
1245
|
it("keeps bare keep-going continuation on the active autopilot skill instead of denying with generic ralph overlap", async () => {
|
|
796
1246
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-bare-continuation-"));
|
|
797
1247
|
try {
|
|
@@ -845,7 +1295,7 @@ describe("codex native hook dispatch", () => {
|
|
|
845
1295
|
}
|
|
846
1296
|
});
|
|
847
1297
|
|
|
848
|
-
it("clarifies
|
|
1298
|
+
it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
|
|
849
1299
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
|
|
850
1300
|
try {
|
|
851
1301
|
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
@@ -868,42 +1318,30 @@ describe("codex native hook dispatch", () => {
|
|
|
868
1318
|
);
|
|
869
1319
|
assert.match(message, /\$deep-interview" -> deep-interview/);
|
|
870
1320
|
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\./);
|
|
871
|
-
assert.match(message, /Deep-interview
|
|
872
|
-
assert.match(message, /
|
|
873
|
-
assert.match(message, /
|
|
874
|
-
assert.match(message, /
|
|
875
|
-
assert.match(message, /
|
|
1321
|
+
assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
|
|
1322
|
+
assert.match(message, /Do not invoke `omx question`, `omx hud`, or `omx team`/);
|
|
1323
|
+
assert.match(message, /native structured question tool when available/);
|
|
1324
|
+
assert.match(message, /ask exactly one concise plain-text question/);
|
|
1325
|
+
assert.match(message, /no tmux question obligation should be created outside tmux/);
|
|
876
1326
|
assert.doesNotMatch(message, /OMX_QUESTION_RETURN_PANE=/);
|
|
877
1327
|
assert.doesNotMatch(message, /preserve the leader pane/i);
|
|
878
|
-
assert.match(message, /Stop remains blocked while a deep-interview question obligation is pending\./);
|
|
879
1328
|
} finally {
|
|
880
1329
|
await rm(cwd, { recursive: true, force: true });
|
|
881
1330
|
}
|
|
882
1331
|
});
|
|
883
1332
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const
|
|
1333
|
+
it("uses native fallback deep-interview guidance on Windows outside tmux", async () => {
|
|
1334
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-win32-"));
|
|
1335
|
+
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
887
1336
|
try {
|
|
888
|
-
|
|
889
|
-
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
890
|
-
await mkdir(sessionDir, { recursive: true });
|
|
891
|
-
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
892
|
-
active: true,
|
|
893
|
-
mode: "deep-interview",
|
|
894
|
-
current_phase: "intent-first",
|
|
895
|
-
started_at: "2026-04-21T10:00:00.000Z",
|
|
896
|
-
updated_at: "2026-04-21T10:00:00.000Z",
|
|
897
|
-
tmux_pane_id: "%77",
|
|
898
|
-
});
|
|
899
|
-
|
|
1337
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
900
1338
|
const result = await dispatchCodexNativeHook(
|
|
901
1339
|
{
|
|
902
1340
|
hook_event_name: "UserPromptSubmit",
|
|
903
1341
|
cwd,
|
|
904
|
-
session_id:
|
|
905
|
-
thread_id: "thread-deep-interview-
|
|
906
|
-
turn_id: "turn-deep-interview-
|
|
1342
|
+
session_id: "sess-deep-interview-msg-win32",
|
|
1343
|
+
thread_id: "thread-deep-interview-msg-win32",
|
|
1344
|
+
turn_id: "turn-deep-interview-msg-win32",
|
|
907
1345
|
prompt: "$deep-interview gather requirements",
|
|
908
1346
|
},
|
|
909
1347
|
{ cwd },
|
|
@@ -914,10 +1352,99 @@ describe("codex native hook dispatch", () => {
|
|
|
914
1352
|
const message = String(
|
|
915
1353
|
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
916
1354
|
);
|
|
917
|
-
assert.match(message, /
|
|
918
|
-
assert.match(message, /
|
|
919
|
-
assert.
|
|
1355
|
+
assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
|
|
1356
|
+
assert.match(message, /native structured question tool when available/);
|
|
1357
|
+
assert.doesNotMatch(message, /OMX_QUESTION_RETURN_PANE=/);
|
|
1358
|
+
assert.doesNotMatch(message, /current-session CLI bridge command/);
|
|
920
1359
|
} finally {
|
|
1360
|
+
if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform);
|
|
1361
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
it("includes leader-pane preservation guidance when a pane hint is available", async () => {
|
|
1367
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-pane-hint-"));
|
|
1368
|
+
try {
|
|
1369
|
+
const sessionId = "sess-deep-interview-pane-hint";
|
|
1370
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
1371
|
+
await mkdir(sessionDir, { recursive: true });
|
|
1372
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
1373
|
+
active: true,
|
|
1374
|
+
mode: "deep-interview",
|
|
1375
|
+
current_phase: "intent-first",
|
|
1376
|
+
started_at: "2026-04-21T10:00:00.000Z",
|
|
1377
|
+
updated_at: "2026-04-21T10:00:00.000Z",
|
|
1378
|
+
tmux_pane_id: "%77",
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
const result = await dispatchCodexNativeHook(
|
|
1382
|
+
{
|
|
1383
|
+
hook_event_name: "UserPromptSubmit",
|
|
1384
|
+
cwd,
|
|
1385
|
+
session_id: sessionId,
|
|
1386
|
+
thread_id: "thread-deep-interview-pane-hint",
|
|
1387
|
+
turn_id: "turn-deep-interview-pane-hint",
|
|
1388
|
+
prompt: "$deep-interview gather requirements",
|
|
1389
|
+
},
|
|
1390
|
+
{ cwd },
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1394
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
1395
|
+
const message = String(
|
|
1396
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
1397
|
+
);
|
|
1398
|
+
assert.match(message, /not attached to tmux/);
|
|
1399
|
+
assert.match(message, /native structured question tool when available/);
|
|
1400
|
+
assert.match(message, /tmux return bridge \(%77\) is recorded/);
|
|
1401
|
+
assert.doesNotMatch(message, /current-session CLI bridge command/);
|
|
1402
|
+
} finally {
|
|
1403
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
it("uses native fallback guidance on Windows when a pane hint is available", async () => {
|
|
1408
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-pane-hint-win32-"));
|
|
1409
|
+
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
1410
|
+
try {
|
|
1411
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
1412
|
+
const sessionId = "sess-deep-interview-pane-hint-win32";
|
|
1413
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
1414
|
+
await mkdir(sessionDir, { recursive: true });
|
|
1415
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
1416
|
+
active: true,
|
|
1417
|
+
mode: "deep-interview",
|
|
1418
|
+
current_phase: "intent-first",
|
|
1419
|
+
started_at: "2026-04-21T10:00:00.000Z",
|
|
1420
|
+
updated_at: "2026-04-21T10:00:00.000Z",
|
|
1421
|
+
tmux_pane_id: "%77",
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
const result = await dispatchCodexNativeHook(
|
|
1425
|
+
{
|
|
1426
|
+
hook_event_name: "UserPromptSubmit",
|
|
1427
|
+
cwd,
|
|
1428
|
+
session_id: sessionId,
|
|
1429
|
+
thread_id: "thread-deep-interview-pane-hint-win32",
|
|
1430
|
+
turn_id: "turn-deep-interview-pane-hint-win32",
|
|
1431
|
+
prompt: "$deep-interview gather requirements",
|
|
1432
|
+
},
|
|
1433
|
+
{ cwd },
|
|
1434
|
+
);
|
|
1435
|
+
|
|
1436
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1437
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
1438
|
+
const message = String(
|
|
1439
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
1440
|
+
);
|
|
1441
|
+
assert.match(message, /not attached to tmux/);
|
|
1442
|
+
assert.match(message, /native structured question tool when available/);
|
|
1443
|
+
assert.match(message, /tmux return bridge \(%77\) is recorded/);
|
|
1444
|
+
assert.doesNotMatch(message, /OMX_QUESTION_RETURN_PANE=/);
|
|
1445
|
+
assert.doesNotMatch(message, /PowerShell\/background-terminal/);
|
|
1446
|
+
} finally {
|
|
1447
|
+
if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform);
|
|
921
1448
|
await rm(cwd, { recursive: true, force: true });
|
|
922
1449
|
}
|
|
923
1450
|
});
|
|
@@ -1090,14 +1617,40 @@ export async function onHookEvent(event) {
|
|
|
1090
1617
|
}
|
|
1091
1618
|
});
|
|
1092
1619
|
|
|
1093
|
-
it("
|
|
1094
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
1620
|
+
it("does not emit UserPromptSubmit routing context for unknown plugin-prefixed $tokens", async () => {
|
|
1621
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-unknown-plugin-token-"));
|
|
1622
|
+
try {
|
|
1623
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1624
|
+
const result = await dispatchCodexNativeHook(
|
|
1625
|
+
{
|
|
1626
|
+
hook_event_name: "UserPromptSubmit",
|
|
1627
|
+
cwd,
|
|
1628
|
+
session_id: "sess-unknown-plugin-1",
|
|
1629
|
+
thread_id: "thread-unknown-plugin-1",
|
|
1630
|
+
turn_id: "turn-unknown-plugin-1",
|
|
1631
|
+
prompt: "$oh-my-codex:maer-thinking 다시 설명해봐",
|
|
1632
|
+
},
|
|
1633
|
+
{ cwd },
|
|
1634
|
+
);
|
|
1635
|
+
|
|
1636
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1637
|
+
assert.equal(result.skillState, null);
|
|
1638
|
+
assert.equal(result.outputJson, null);
|
|
1639
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
1640
|
+
} finally {
|
|
1641
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
it("denies direct $team prompt activation from Codex App/native outside tmux", async () => {
|
|
1646
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-native-block-"));
|
|
1095
1647
|
try {
|
|
1096
1648
|
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1097
1649
|
const result = await dispatchCodexNativeHook(
|
|
1098
1650
|
{
|
|
1099
1651
|
hook_event_name: "UserPromptSubmit",
|
|
1100
1652
|
cwd,
|
|
1653
|
+
source: "codex-app",
|
|
1101
1654
|
session_id: "sess-team-1",
|
|
1102
1655
|
thread_id: "thread-team-1",
|
|
1103
1656
|
turn_id: "turn-team-1",
|
|
@@ -1108,19 +1661,105 @@ export async function onHookEvent(event) {
|
|
|
1108
1661
|
|
|
1109
1662
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
1110
1663
|
assert.equal(result.skillState?.skill, "team");
|
|
1111
|
-
assert.
|
|
1112
|
-
|
|
1113
|
-
|
|
1664
|
+
assert.equal(result.skillState?.active, false);
|
|
1665
|
+
assert.match(String(result.skillState?.transition_error || ""), /cannot activate the tmux-only `team` workflow directly/);
|
|
1666
|
+
const message = String(
|
|
1667
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null)?.hookSpecificOutput?.additionalContext ?? "",
|
|
1668
|
+
);
|
|
1669
|
+
assert.match(message, /denied workflow keyword "\$team" -> team/);
|
|
1670
|
+
assert.match(message, /attached tmux shell first/);
|
|
1671
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
|
|
1672
|
+
} finally {
|
|
1673
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
it("still denies direct $team prompt activation from Codex App/native outside tmux when a tmux return bridge exists", async () => {
|
|
1678
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-native-bridge-block-"));
|
|
1679
|
+
try {
|
|
1680
|
+
await mkdir(join(cwd, ".omx", "state", "sessions", "sess-team-bridge"), { recursive: true });
|
|
1681
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", "sess-team-bridge", "ralph-state.json"), {
|
|
1682
|
+
mode: "ralph",
|
|
1683
|
+
active: true,
|
|
1684
|
+
tmux_pane_id: "%42",
|
|
1685
|
+
});
|
|
1686
|
+
const result = await dispatchCodexNativeHook(
|
|
1687
|
+
{
|
|
1688
|
+
hook_event_name: "UserPromptSubmit",
|
|
1689
|
+
cwd,
|
|
1690
|
+
source: "codex-app",
|
|
1691
|
+
session_id: "sess-team-bridge",
|
|
1692
|
+
thread_id: "thread-team-bridge",
|
|
1693
|
+
turn_id: "turn-team-bridge",
|
|
1694
|
+
prompt: "$team ship this fix with verification",
|
|
1695
|
+
},
|
|
1696
|
+
{ cwd },
|
|
1114
1697
|
);
|
|
1115
|
-
assert.match(JSON.stringify(result.outputJson), /Use the durable OMX team runtime via `omx team \.\.\.`/);
|
|
1116
|
-
assert.match(JSON.stringify(result.outputJson), /If you need runtime syntax, run `omx team --help` yourself\./);
|
|
1117
1698
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1699
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1700
|
+
assert.equal(result.skillState?.skill, "team");
|
|
1701
|
+
assert.equal(result.skillState?.active, false);
|
|
1702
|
+
const message = String(
|
|
1703
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null)?.hookSpecificOutput?.additionalContext ?? "",
|
|
1704
|
+
);
|
|
1705
|
+
assert.match(message, /attached tmux shell first/);
|
|
1706
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "team-state.json")), false);
|
|
1707
|
+
} finally {
|
|
1708
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
it("keeps direct CLI outside-tmux $team prompt guidance compatible with manual shell launch", async () => {
|
|
1713
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-cli-guidance-"));
|
|
1714
|
+
try {
|
|
1715
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1716
|
+
const result = await dispatchCodexNativeHook(
|
|
1717
|
+
{
|
|
1718
|
+
hook_event_name: "UserPromptSubmit",
|
|
1719
|
+
cwd,
|
|
1720
|
+
source: "cli",
|
|
1721
|
+
session_id: "sess-team-cli-guidance",
|
|
1722
|
+
thread_id: "thread-team-cli-guidance",
|
|
1723
|
+
turn_id: "turn-team-cli-guidance",
|
|
1724
|
+
prompt: "$team ship this fix with verification",
|
|
1725
|
+
},
|
|
1726
|
+
{ cwd },
|
|
1727
|
+
);
|
|
1728
|
+
|
|
1729
|
+
const message = String(
|
|
1730
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null)?.hookSpecificOutput?.additionalContext ?? "",
|
|
1731
|
+
);
|
|
1732
|
+
assert.match(message, /run `omx team \.\.\.` yourself from shell/);
|
|
1733
|
+
assert.doesNotMatch(message, /not directly available here/);
|
|
1734
|
+
} finally {
|
|
1735
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
it("keeps $team prompt-submit routing directly tmux-capable when already inside tmux", async () => {
|
|
1740
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-tmux-"));
|
|
1741
|
+
process.env.TMUX = "/tmp/tmux-live";
|
|
1742
|
+
process.env.TMUX_PANE = "%5";
|
|
1743
|
+
try {
|
|
1744
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1745
|
+
const result = await dispatchCodexNativeHook(
|
|
1746
|
+
{
|
|
1747
|
+
hook_event_name: "UserPromptSubmit",
|
|
1748
|
+
cwd,
|
|
1749
|
+
session_id: "sess-team-tmux-1",
|
|
1750
|
+
thread_id: "thread-team-tmux-1",
|
|
1751
|
+
turn_id: "turn-team-tmux-1",
|
|
1752
|
+
prompt: "$team ship this fix with verification",
|
|
1753
|
+
},
|
|
1754
|
+
{ cwd },
|
|
1755
|
+
);
|
|
1756
|
+
|
|
1757
|
+
const message = String(
|
|
1758
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
1759
|
+
);
|
|
1760
|
+
assert.match(message, /Use the durable OMX team runtime via `omx team \.\.\.`/);
|
|
1761
|
+
assert.match(message, /run `omx team --help` yourself/);
|
|
1762
|
+
assert.doesNotMatch(message, /not directly available here/);
|
|
1124
1763
|
} finally {
|
|
1125
1764
|
await rm(cwd, { recursive: true, force: true });
|
|
1126
1765
|
}
|
|
@@ -1241,6 +1880,37 @@ export async function onHookEvent(event) {
|
|
|
1241
1880
|
}
|
|
1242
1881
|
});
|
|
1243
1882
|
|
|
1883
|
+
it("keeps the planning skill active for mixed plugin-prefixed and bare workflow invocations together", async () => {
|
|
1884
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-planning-precedence-"));
|
|
1885
|
+
try {
|
|
1886
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1887
|
+
|
|
1888
|
+
const result = await dispatchCodexNativeHook(
|
|
1889
|
+
{
|
|
1890
|
+
hook_event_name: "UserPromptSubmit",
|
|
1891
|
+
cwd,
|
|
1892
|
+
session_id: "sess-plugin-multi-1",
|
|
1893
|
+
thread_id: "thread-plugin-multi-1",
|
|
1894
|
+
turn_id: "turn-plugin-multi-1",
|
|
1895
|
+
prompt: "$oh-my-codex:ralplan $team $oh-my-codex:ralph ship this fix",
|
|
1896
|
+
},
|
|
1897
|
+
{ cwd },
|
|
1898
|
+
);
|
|
1899
|
+
|
|
1900
|
+
const message = String(
|
|
1901
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || '',
|
|
1902
|
+
);
|
|
1903
|
+
assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
|
|
1904
|
+
assert.match(message, /\$team" -> team/);
|
|
1905
|
+
assert.match(message, /\$oh-my-codex:ralph" -> ralph/);
|
|
1906
|
+
assert.doesNotMatch(message, /mode transiting:/);
|
|
1907
|
+
assert.match(message, /planning preserved over simultaneous execution follow-up; deferred skills: team, ralph\./);
|
|
1908
|
+
assert.match(message, /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-multi-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
|
|
1909
|
+
} finally {
|
|
1910
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1244
1914
|
it("runs prompt-submit HUD reconciliation as a best-effort tmux-only side effect", async () => {
|
|
1245
1915
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-"));
|
|
1246
1916
|
const originalTmux = process.env.TMUX;
|
|
@@ -1380,154 +2050,451 @@ esac
|
|
|
1380
2050
|
}
|
|
1381
2051
|
});
|
|
1382
2052
|
|
|
1383
|
-
it("
|
|
1384
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-
|
|
2053
|
+
it("allows PowerShell env bridge forms for omx question return panes", async () => {
|
|
2054
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-powershell-allow-"));
|
|
2055
|
+
try {
|
|
2056
|
+
const commands = [
|
|
2057
|
+
`$env:OMX_QUESTION_RETURN_PANE=$env:TMUX_PANE; omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'`,
|
|
2058
|
+
`$env:OMX_QUESTION_RETURN_PANE='%42'; node ./dist/cli/omx.js question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'`,
|
|
2059
|
+
`$env:OMX_LEADER_PANE_ID="%43"; omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'`,
|
|
2060
|
+
];
|
|
2061
|
+
|
|
2062
|
+
for (const [index, command] of commands.entries()) {
|
|
2063
|
+
const result = await dispatchCodexNativeHook(
|
|
2064
|
+
{
|
|
2065
|
+
hook_event_name: "PreToolUse",
|
|
2066
|
+
cwd,
|
|
2067
|
+
tool_name: "Bash",
|
|
2068
|
+
tool_use_id: `tool-question-powershell-allow-${index}`,
|
|
2069
|
+
tool_input: { command },
|
|
2070
|
+
},
|
|
2071
|
+
{ cwd },
|
|
2072
|
+
);
|
|
2073
|
+
|
|
2074
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2075
|
+
assert.equal(result.outputJson, null);
|
|
2076
|
+
}
|
|
2077
|
+
} finally {
|
|
2078
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
it("allows Bash omx question when a valid inherited OMX_QUESTION_RETURN_PANE bridge is already exported", async () => {
|
|
2083
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-env-allow-"));
|
|
2084
|
+
const originalReturnPane = process.env.OMX_QUESTION_RETURN_PANE;
|
|
1385
2085
|
try {
|
|
2086
|
+
process.env.OMX_QUESTION_RETURN_PANE = "%42";
|
|
1386
2087
|
const result = await dispatchCodexNativeHook(
|
|
1387
2088
|
{
|
|
1388
2089
|
hook_event_name: "PreToolUse",
|
|
1389
2090
|
cwd,
|
|
1390
2091
|
tool_name: "Bash",
|
|
1391
|
-
tool_use_id: "tool-question-
|
|
1392
|
-
tool_input: { command: `
|
|
2092
|
+
tool_use_id: "tool-question-env-allow",
|
|
2093
|
+
tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
|
|
1393
2094
|
},
|
|
1394
2095
|
{ cwd },
|
|
1395
2096
|
);
|
|
1396
2097
|
|
|
1397
2098
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1398
|
-
assert.equal(
|
|
2099
|
+
assert.equal(result.outputJson, null);
|
|
1399
2100
|
} finally {
|
|
2101
|
+
if (originalReturnPane === undefined) delete process.env.OMX_QUESTION_RETURN_PANE;
|
|
2102
|
+
else process.env.OMX_QUESTION_RETURN_PANE = originalReturnPane;
|
|
1400
2103
|
await rm(cwd, { recursive: true, force: true });
|
|
1401
2104
|
}
|
|
1402
2105
|
});
|
|
1403
2106
|
|
|
1404
|
-
it("
|
|
1405
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2107
|
+
it("allows Bash omx question when a valid inherited OMX_LEADER_PANE_ID bridge is already exported", async () => {
|
|
2108
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-leader-env-allow-"));
|
|
2109
|
+
const originalLeaderPane = process.env.OMX_LEADER_PANE_ID;
|
|
1406
2110
|
try {
|
|
2111
|
+
process.env.OMX_LEADER_PANE_ID = "%43";
|
|
1407
2112
|
const result = await dispatchCodexNativeHook(
|
|
1408
2113
|
{
|
|
1409
2114
|
hook_event_name: "PreToolUse",
|
|
1410
2115
|
cwd,
|
|
1411
2116
|
tool_name: "Bash",
|
|
1412
|
-
tool_use_id: "tool-
|
|
1413
|
-
tool_input: { command:
|
|
2117
|
+
tool_use_id: "tool-question-leader-env-allow",
|
|
2118
|
+
tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
|
|
1414
2119
|
},
|
|
1415
2120
|
{ cwd },
|
|
1416
2121
|
);
|
|
1417
2122
|
|
|
1418
2123
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1419
|
-
assert.
|
|
1420
|
-
hookSpecificOutput: {
|
|
1421
|
-
hookEventName: "PreToolUse",
|
|
1422
|
-
},
|
|
1423
|
-
systemMessage:
|
|
1424
|
-
"Destructive Bash command detected (`rm -rf dist`). Confirm the target and expected side effects before running it.",
|
|
1425
|
-
});
|
|
2124
|
+
assert.equal(result.outputJson, null);
|
|
1426
2125
|
} finally {
|
|
2126
|
+
if (originalLeaderPane === undefined) delete process.env.OMX_LEADER_PANE_ID;
|
|
2127
|
+
else process.env.OMX_LEADER_PANE_ID = originalLeaderPane;
|
|
1427
2128
|
await rm(cwd, { recursive: true, force: true });
|
|
1428
2129
|
}
|
|
1429
2130
|
});
|
|
1430
2131
|
|
|
1431
|
-
it("
|
|
1432
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2132
|
+
it("still blocks Bash omx question when an inherited OMX_QUESTION_RETURN_PANE value is malformed", async () => {
|
|
2133
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-env-malformed-"));
|
|
2134
|
+
const originalReturnPane = process.env.OMX_QUESTION_RETURN_PANE;
|
|
1433
2135
|
try {
|
|
2136
|
+
process.env.OMX_QUESTION_RETURN_PANE = "not-a-pane";
|
|
1434
2137
|
const result = await dispatchCodexNativeHook(
|
|
1435
2138
|
{
|
|
1436
2139
|
hook_event_name: "PreToolUse",
|
|
1437
2140
|
cwd,
|
|
1438
2141
|
tool_name: "Bash",
|
|
1439
|
-
tool_use_id: "tool-
|
|
1440
|
-
tool_input: { command: "
|
|
2142
|
+
tool_use_id: "tool-question-env-malformed",
|
|
2143
|
+
tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
|
|
1441
2144
|
},
|
|
1442
2145
|
{ cwd },
|
|
1443
2146
|
);
|
|
1444
2147
|
|
|
1445
2148
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1446
|
-
assert.equal(result.outputJson
|
|
2149
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
1447
2150
|
} finally {
|
|
2151
|
+
if (originalReturnPane === undefined) delete process.env.OMX_QUESTION_RETURN_PANE;
|
|
2152
|
+
else process.env.OMX_QUESTION_RETURN_PANE = originalReturnPane;
|
|
1448
2153
|
await rm(cwd, { recursive: true, force: true });
|
|
1449
2154
|
}
|
|
1450
2155
|
});
|
|
1451
2156
|
|
|
1452
|
-
it("blocks
|
|
1453
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2157
|
+
it("blocks Bash node omx.js question when the command does not preserve the leader-pane return hint", async () => {
|
|
2158
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-node-block-"));
|
|
1454
2159
|
try {
|
|
1455
2160
|
const result = await dispatchCodexNativeHook(
|
|
1456
2161
|
{
|
|
1457
2162
|
hook_event_name: "PreToolUse",
|
|
1458
2163
|
cwd,
|
|
1459
2164
|
tool_name: "Bash",
|
|
1460
|
-
tool_use_id: "tool-
|
|
1461
|
-
tool_input: { command:
|
|
2165
|
+
tool_use_id: "tool-question-node-block",
|
|
2166
|
+
tool_input: { command: `node ./dist/cli/omx.js question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
|
|
1462
2167
|
},
|
|
1463
2168
|
{ cwd },
|
|
1464
2169
|
);
|
|
1465
2170
|
|
|
1466
2171
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1467
|
-
assert.
|
|
1468
|
-
decision: "block",
|
|
1469
|
-
reason:
|
|
1470
|
-
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1471
|
-
hookSpecificOutput: {
|
|
1472
|
-
hookEventName: "PreToolUse",
|
|
1473
|
-
additionalContext: [
|
|
1474
|
-
"Lore-format git commit enforcement triggered.",
|
|
1475
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1476
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1477
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1478
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1479
|
-
].join("\n"),
|
|
1480
|
-
},
|
|
1481
|
-
systemMessage: [
|
|
1482
|
-
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1483
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1484
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1485
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1486
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1487
|
-
].join("\n"),
|
|
1488
|
-
});
|
|
2172
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
1489
2173
|
} finally {
|
|
1490
2174
|
await rm(cwd, { recursive: true, force: true });
|
|
1491
2175
|
}
|
|
1492
2176
|
});
|
|
1493
2177
|
|
|
1494
|
-
it("
|
|
1495
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2178
|
+
it("blocks native/App Bash omx question with bridge-specific outside-tmux guidance", async () => {
|
|
2179
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-native-block-"));
|
|
1496
2180
|
try {
|
|
1497
2181
|
const result = await dispatchCodexNativeHook(
|
|
1498
2182
|
{
|
|
1499
2183
|
hook_event_name: "PreToolUse",
|
|
1500
2184
|
cwd,
|
|
2185
|
+
source: "codex-app",
|
|
2186
|
+
session_id: "sess-question-native-block",
|
|
1501
2187
|
tool_name: "Bash",
|
|
1502
|
-
tool_use_id: "tool-
|
|
1503
|
-
tool_input: { command:
|
|
2188
|
+
tool_use_id: "tool-question-native-block",
|
|
2189
|
+
tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
|
|
1504
2190
|
},
|
|
1505
2191
|
{ cwd },
|
|
1506
2192
|
);
|
|
1507
2193
|
|
|
1508
2194
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1509
|
-
assert.equal(result.outputJson
|
|
2195
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2196
|
+
assert.equal((result.outputJson as { hookSpecificOutput?: unknown } | null)?.hookSpecificOutput, undefined);
|
|
2197
|
+
assert.match(String((result.outputJson as { reason?: string } | null)?.reason || ""), /Codex App\/native outside-tmux Bash sessions/);
|
|
2198
|
+
assert.match(String((result.outputJson as { systemMessage?: string } | null)?.systemMessage || ""), /native structured question tool/);
|
|
1510
2199
|
} finally {
|
|
1511
2200
|
await rm(cwd, { recursive: true, force: true });
|
|
1512
2201
|
}
|
|
1513
2202
|
});
|
|
1514
2203
|
|
|
1515
|
-
it("
|
|
1516
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2204
|
+
it("blocks native/App Bash omx question even when the command preserves a tmux return bridge", async () => {
|
|
2205
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-native-allow-"));
|
|
1517
2206
|
try {
|
|
1518
2207
|
const result = await dispatchCodexNativeHook(
|
|
1519
2208
|
{
|
|
1520
2209
|
hook_event_name: "PreToolUse",
|
|
1521
2210
|
cwd,
|
|
2211
|
+
source: "codex-app",
|
|
2212
|
+
session_id: "sess-question-native-bridge-block",
|
|
1522
2213
|
tool_name: "Bash",
|
|
1523
|
-
tool_use_id: "tool-
|
|
1524
|
-
tool_input: { command:
|
|
2214
|
+
tool_use_id: "tool-question-native-bridge-block",
|
|
2215
|
+
tool_input: { command: `OMX_QUESTION_RETURN_PANE=$TMUX_PANE omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
|
|
1525
2216
|
},
|
|
1526
2217
|
{ cwd },
|
|
1527
2218
|
);
|
|
1528
2219
|
|
|
1529
2220
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1530
|
-
assert.equal(result.outputJson
|
|
2221
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2222
|
+
assert.match(String((result.outputJson as { systemMessage?: string } | null)?.systemMessage || ""), /native structured question tool/);
|
|
2223
|
+
} finally {
|
|
2224
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
it("blocks native/App Bash omx question when a valid inherited OMX_QUESTION_RETURN_PANE bridge is already exported", async () => {
|
|
2229
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-native-env-allow-"));
|
|
2230
|
+
const originalReturnPane = process.env.OMX_QUESTION_RETURN_PANE;
|
|
2231
|
+
try {
|
|
2232
|
+
process.env.OMX_QUESTION_RETURN_PANE = "%42";
|
|
2233
|
+
const result = await dispatchCodexNativeHook(
|
|
2234
|
+
{
|
|
2235
|
+
hook_event_name: "PreToolUse",
|
|
2236
|
+
cwd,
|
|
2237
|
+
source: "codex-app",
|
|
2238
|
+
session_id: "sess-question-native-env-allow",
|
|
2239
|
+
tool_name: "Bash",
|
|
2240
|
+
tool_use_id: "tool-question-native-env-allow",
|
|
2241
|
+
tool_input: { command: `omx question --json --input '{"question":"Q?","options":["A"],"allow_other":true}'` },
|
|
2242
|
+
},
|
|
2243
|
+
{ cwd },
|
|
2244
|
+
);
|
|
2245
|
+
|
|
2246
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2247
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2248
|
+
} finally {
|
|
2249
|
+
if (originalReturnPane === undefined) delete process.env.OMX_QUESTION_RETURN_PANE;
|
|
2250
|
+
else process.env.OMX_QUESTION_RETURN_PANE = originalReturnPane;
|
|
2251
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
2254
|
+
|
|
2255
|
+
it("blocks Bash omx hud from Codex App/native outside tmux without PreToolUse additionalContext", async () => {
|
|
2256
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-hud-native-block-"));
|
|
2257
|
+
try {
|
|
2258
|
+
const result = await dispatchCodexNativeHook(
|
|
2259
|
+
{
|
|
2260
|
+
hook_event_name: "PreToolUse",
|
|
2261
|
+
cwd,
|
|
2262
|
+
source: "codex-app",
|
|
2263
|
+
session_id: "sess-hud-native-block",
|
|
2264
|
+
tool_name: "Bash",
|
|
2265
|
+
tool_use_id: "tool-hud-native-block",
|
|
2266
|
+
tool_input: { command: "omx hud --tmux" },
|
|
2267
|
+
},
|
|
2268
|
+
{ cwd },
|
|
2269
|
+
);
|
|
2270
|
+
|
|
2271
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2272
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2273
|
+
assert.equal((result.outputJson as { hookSpecificOutput?: unknown } | null)?.hookSpecificOutput, undefined);
|
|
2274
|
+
assert.match(String((result.outputJson as { systemMessage?: string } | null)?.systemMessage || ""), /attached tmux shell first/);
|
|
2275
|
+
} finally {
|
|
2276
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
it("blocks Bash omx team from Codex App/native outside tmux", async () => {
|
|
2281
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-native-block-"));
|
|
2282
|
+
try {
|
|
2283
|
+
const result = await dispatchCodexNativeHook(
|
|
2284
|
+
{
|
|
2285
|
+
hook_event_name: "PreToolUse",
|
|
2286
|
+
cwd,
|
|
2287
|
+
source: "codex-app",
|
|
2288
|
+
session_id: "sess-team-native-block",
|
|
2289
|
+
tool_name: "Bash",
|
|
2290
|
+
tool_use_id: "tool-team-native-block",
|
|
2291
|
+
tool_input: { command: "omx team status my-team" },
|
|
2292
|
+
},
|
|
2293
|
+
{ cwd },
|
|
2294
|
+
);
|
|
2295
|
+
|
|
2296
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2297
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2298
|
+
assert.equal((result.outputJson as { hookSpecificOutput?: unknown } | null)?.hookSpecificOutput, undefined);
|
|
2299
|
+
assert.match(String((result.outputJson as { reason?: string } | null)?.reason || ""), /cannot be launched directly from Codex App\/native outside-tmux Bash sessions/);
|
|
2300
|
+
assert.match(String((result.outputJson as { systemMessage?: string } | null)?.systemMessage || ""), /launch OMX CLI from an attached tmux shell first/);
|
|
2301
|
+
} finally {
|
|
2302
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2303
|
+
}
|
|
2304
|
+
});
|
|
2305
|
+
|
|
2306
|
+
it("blocks Bash node omx.js team from Codex App/native outside tmux", async () => {
|
|
2307
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-node-native-block-"));
|
|
2308
|
+
try {
|
|
2309
|
+
const result = await dispatchCodexNativeHook(
|
|
2310
|
+
{
|
|
2311
|
+
hook_event_name: "PreToolUse",
|
|
2312
|
+
cwd,
|
|
2313
|
+
source: "codex-app",
|
|
2314
|
+
session_id: "sess-team-node-native-block",
|
|
2315
|
+
tool_name: "Bash",
|
|
2316
|
+
tool_use_id: "tool-team-node-native-block",
|
|
2317
|
+
tool_input: { command: "node ./dist/cli/omx.js team status my-team" },
|
|
2318
|
+
},
|
|
2319
|
+
{ cwd },
|
|
2320
|
+
);
|
|
2321
|
+
|
|
2322
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2323
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2324
|
+
assert.match(String((result.outputJson as { systemMessage?: string } | null)?.systemMessage || ""), /Codex App\/native outside-tmux sessions/);
|
|
2325
|
+
} finally {
|
|
2326
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
it("preserves direct CLI outside-tmux omx team Bash behavior", async () => {
|
|
2331
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-cli-outside-"));
|
|
2332
|
+
try {
|
|
2333
|
+
const result = await dispatchCodexNativeHook(
|
|
2334
|
+
{
|
|
2335
|
+
hook_event_name: "PreToolUse",
|
|
2336
|
+
cwd,
|
|
2337
|
+
source: "cli",
|
|
2338
|
+
session_id: "sess-team-cli-outside",
|
|
2339
|
+
tool_name: "Bash",
|
|
2340
|
+
tool_use_id: "tool-team-cli-outside",
|
|
2341
|
+
tool_input: { command: "omx team status my-team" },
|
|
2342
|
+
},
|
|
2343
|
+
{ cwd },
|
|
2344
|
+
);
|
|
2345
|
+
|
|
2346
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2347
|
+
assert.equal(result.outputJson, null);
|
|
2348
|
+
} finally {
|
|
2349
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
it("preserves source-less outside-tmux omx team Bash behavior when no native session evidence exists", async () => {
|
|
2354
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-team-cli-nosource-"));
|
|
2355
|
+
try {
|
|
2356
|
+
const result = await dispatchCodexNativeHook(
|
|
2357
|
+
{
|
|
2358
|
+
hook_event_name: "PreToolUse",
|
|
2359
|
+
cwd,
|
|
2360
|
+
session_id: "sess-team-cli-nosource",
|
|
2361
|
+
tool_name: "Bash",
|
|
2362
|
+
tool_use_id: "tool-team-cli-nosource",
|
|
2363
|
+
tool_input: { command: "omx team status my-team" },
|
|
2364
|
+
},
|
|
2365
|
+
{ cwd },
|
|
2366
|
+
);
|
|
2367
|
+
|
|
2368
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2369
|
+
assert.equal(result.outputJson, null);
|
|
2370
|
+
} finally {
|
|
2371
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
it("returns a destructive-command caution on PreToolUse for rm -rf dist", async () => {
|
|
2376
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-danger-"));
|
|
2377
|
+
try {
|
|
2378
|
+
const result = await dispatchCodexNativeHook(
|
|
2379
|
+
{
|
|
2380
|
+
hook_event_name: "PreToolUse",
|
|
2381
|
+
cwd,
|
|
2382
|
+
tool_name: "Bash",
|
|
2383
|
+
tool_use_id: "tool-danger",
|
|
2384
|
+
tool_input: { command: "rm -rf dist" },
|
|
2385
|
+
},
|
|
2386
|
+
{ cwd },
|
|
2387
|
+
);
|
|
2388
|
+
|
|
2389
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2390
|
+
assert.deepEqual(result.outputJson, {
|
|
2391
|
+
hookSpecificOutput: {
|
|
2392
|
+
hookEventName: "PreToolUse",
|
|
2393
|
+
},
|
|
2394
|
+
systemMessage:
|
|
2395
|
+
"Destructive Bash command detected (`rm -rf dist`). Confirm the target and expected side effects before running it.",
|
|
2396
|
+
});
|
|
2397
|
+
} finally {
|
|
2398
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
it("stays silent on PreToolUse for neutral pwd", async () => {
|
|
2403
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-neutral-"));
|
|
2404
|
+
try {
|
|
2405
|
+
const result = await dispatchCodexNativeHook(
|
|
2406
|
+
{
|
|
2407
|
+
hook_event_name: "PreToolUse",
|
|
2408
|
+
cwd,
|
|
2409
|
+
tool_name: "Bash",
|
|
2410
|
+
tool_use_id: "tool-neutral",
|
|
2411
|
+
tool_input: { command: "pwd" },
|
|
2412
|
+
},
|
|
2413
|
+
{ cwd },
|
|
2414
|
+
);
|
|
2415
|
+
|
|
2416
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2417
|
+
assert.equal(result.outputJson, null);
|
|
2418
|
+
} finally {
|
|
2419
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
|
|
2424
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
|
|
2425
|
+
try {
|
|
2426
|
+
const result = await dispatchCodexNativeHook(
|
|
2427
|
+
{
|
|
2428
|
+
hook_event_name: "PreToolUse",
|
|
2429
|
+
cwd,
|
|
2430
|
+
tool_name: "Bash",
|
|
2431
|
+
tool_use_id: "tool-git-commit-invalid",
|
|
2432
|
+
tool_input: { command: 'git commit -m "fix tests"' },
|
|
2433
|
+
},
|
|
2434
|
+
{ cwd },
|
|
2435
|
+
);
|
|
2436
|
+
|
|
2437
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2438
|
+
assert.deepEqual(result.outputJson, {
|
|
2439
|
+
decision: "block",
|
|
2440
|
+
reason:
|
|
2441
|
+
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
2442
|
+
hookSpecificOutput: {
|
|
2443
|
+
hookEventName: "PreToolUse",
|
|
2444
|
+
},
|
|
2445
|
+
systemMessage: [
|
|
2446
|
+
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
2447
|
+
"- Add a blank line after the subject before the narrative body.",
|
|
2448
|
+
"- Add a narrative body paragraph explaining the decision context.",
|
|
2449
|
+
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
2450
|
+
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
2451
|
+
].join("\n"),
|
|
2452
|
+
});
|
|
2453
|
+
const hookSpecificOutput = (result.outputJson as { hookSpecificOutput?: Record<string, unknown> })
|
|
2454
|
+
.hookSpecificOutput ?? {};
|
|
2455
|
+
assert.equal("additionalContext" in hookSpecificOutput, false);
|
|
2456
|
+
} finally {
|
|
2457
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
it("stays silent on PreToolUse for `git help commit`", async () => {
|
|
2462
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
|
|
2463
|
+
try {
|
|
2464
|
+
const result = await dispatchCodexNativeHook(
|
|
2465
|
+
{
|
|
2466
|
+
hook_event_name: "PreToolUse",
|
|
2467
|
+
cwd,
|
|
2468
|
+
tool_name: "Bash",
|
|
2469
|
+
tool_use_id: "tool-git-help-commit",
|
|
2470
|
+
tool_input: { command: "git help commit" },
|
|
2471
|
+
},
|
|
2472
|
+
{ cwd },
|
|
2473
|
+
);
|
|
2474
|
+
|
|
2475
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2476
|
+
assert.equal(result.outputJson, null);
|
|
2477
|
+
} finally {
|
|
2478
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
|
|
2483
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
|
|
2484
|
+
try {
|
|
2485
|
+
const result = await dispatchCodexNativeHook(
|
|
2486
|
+
{
|
|
2487
|
+
hook_event_name: "PreToolUse",
|
|
2488
|
+
cwd,
|
|
2489
|
+
tool_name: "Bash",
|
|
2490
|
+
tool_use_id: "tool-git-config-alias-commit",
|
|
2491
|
+
tool_input: { command: "git config alias.ci commit" },
|
|
2492
|
+
},
|
|
2493
|
+
{ cwd },
|
|
2494
|
+
);
|
|
2495
|
+
|
|
2496
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2497
|
+
assert.equal(result.outputJson, null);
|
|
1531
2498
|
} finally {
|
|
1532
2499
|
await rm(cwd, { recursive: true, force: true });
|
|
1533
2500
|
}
|
|
@@ -1575,13 +2542,6 @@ esac
|
|
|
1575
2542
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1576
2543
|
hookSpecificOutput: {
|
|
1577
2544
|
hookEventName: "PreToolUse",
|
|
1578
|
-
additionalContext: [
|
|
1579
|
-
"Lore-format git commit enforcement triggered.",
|
|
1580
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1581
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1582
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1583
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1584
|
-
].join("\n"),
|
|
1585
2545
|
},
|
|
1586
2546
|
systemMessage: [
|
|
1587
2547
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1617,13 +2577,6 @@ esac
|
|
|
1617
2577
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1618
2578
|
hookSpecificOutput: {
|
|
1619
2579
|
hookEventName: "PreToolUse",
|
|
1620
|
-
additionalContext: [
|
|
1621
|
-
"Lore-format git commit enforcement triggered.",
|
|
1622
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1623
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1624
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1625
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1626
|
-
].join("\n"),
|
|
1627
2580
|
},
|
|
1628
2581
|
systemMessage: [
|
|
1629
2582
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1659,13 +2612,6 @@ esac
|
|
|
1659
2612
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1660
2613
|
hookSpecificOutput: {
|
|
1661
2614
|
hookEventName: "PreToolUse",
|
|
1662
|
-
additionalContext: [
|
|
1663
|
-
"Lore-format git commit enforcement triggered.",
|
|
1664
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1665
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1666
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1667
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1668
|
-
].join("\n"),
|
|
1669
2615
|
},
|
|
1670
2616
|
systemMessage: [
|
|
1671
2617
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1701,13 +2647,6 @@ esac
|
|
|
1701
2647
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1702
2648
|
hookSpecificOutput: {
|
|
1703
2649
|
hookEventName: "PreToolUse",
|
|
1704
|
-
additionalContext: [
|
|
1705
|
-
"Lore-format git commit enforcement triggered.",
|
|
1706
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1707
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1708
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1709
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1710
|
-
].join("\n"),
|
|
1711
2650
|
},
|
|
1712
2651
|
systemMessage: [
|
|
1713
2652
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1743,13 +2682,6 @@ esac
|
|
|
1743
2682
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1744
2683
|
hookSpecificOutput: {
|
|
1745
2684
|
hookEventName: "PreToolUse",
|
|
1746
|
-
additionalContext: [
|
|
1747
|
-
"Lore-format git commit enforcement triggered.",
|
|
1748
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1749
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1750
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1751
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1752
|
-
].join("\n"),
|
|
1753
2685
|
},
|
|
1754
2686
|
systemMessage: [
|
|
1755
2687
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1785,13 +2717,6 @@ esac
|
|
|
1785
2717
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1786
2718
|
hookSpecificOutput: {
|
|
1787
2719
|
hookEventName: "PreToolUse",
|
|
1788
|
-
additionalContext: [
|
|
1789
|
-
"Lore-format git commit enforcement triggered.",
|
|
1790
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1791
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1792
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1793
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1794
|
-
].join("\n"),
|
|
1795
2720
|
},
|
|
1796
2721
|
systemMessage: [
|
|
1797
2722
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1827,13 +2752,6 @@ esac
|
|
|
1827
2752
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1828
2753
|
hookSpecificOutput: {
|
|
1829
2754
|
hookEventName: "PreToolUse",
|
|
1830
|
-
additionalContext: [
|
|
1831
|
-
"Lore-format git commit enforcement triggered.",
|
|
1832
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1833
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1834
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1835
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1836
|
-
].join("\n"),
|
|
1837
2755
|
},
|
|
1838
2756
|
systemMessage: [
|
|
1839
2757
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1869,13 +2787,6 @@ esac
|
|
|
1869
2787
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1870
2788
|
hookSpecificOutput: {
|
|
1871
2789
|
hookEventName: "PreToolUse",
|
|
1872
|
-
additionalContext: [
|
|
1873
|
-
"Lore-format git commit enforcement triggered.",
|
|
1874
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1875
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1876
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1877
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1878
|
-
].join("\n"),
|
|
1879
2790
|
},
|
|
1880
2791
|
systemMessage: [
|
|
1881
2792
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1911,13 +2822,6 @@ esac
|
|
|
1911
2822
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1912
2823
|
hookSpecificOutput: {
|
|
1913
2824
|
hookEventName: "PreToolUse",
|
|
1914
|
-
additionalContext: [
|
|
1915
|
-
"Lore-format git commit enforcement triggered.",
|
|
1916
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1917
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1918
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1919
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1920
|
-
].join("\n"),
|
|
1921
2825
|
},
|
|
1922
2826
|
systemMessage: [
|
|
1923
2827
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1953,10 +2857,6 @@ esac
|
|
|
1953
2857
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1954
2858
|
hookSpecificOutput: {
|
|
1955
2859
|
hookEventName: "PreToolUse",
|
|
1956
|
-
additionalContext: [
|
|
1957
|
-
"Lore-format git commit enforcement triggered.",
|
|
1958
|
-
"- Use inline `git commit -m ...` paragraphs for Lore-format commits in this path; file/editor/reuse/fixup message sources are not inspectable safely from pre-tool-use enforcement.",
|
|
1959
|
-
].join("\n"),
|
|
1960
2860
|
},
|
|
1961
2861
|
systemMessage: [
|
|
1962
2862
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -1997,10 +2897,6 @@ esac
|
|
|
1997
2897
|
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
1998
2898
|
hookSpecificOutput: {
|
|
1999
2899
|
hookEventName: "PreToolUse",
|
|
2000
|
-
additionalContext: [
|
|
2001
|
-
"Lore-format git commit enforcement triggered.",
|
|
2002
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
2003
|
-
].join("\n"),
|
|
2004
2900
|
},
|
|
2005
2901
|
systemMessage: [
|
|
2006
2902
|
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
@@ -2042,23 +2938,198 @@ esac
|
|
|
2042
2938
|
}
|
|
2043
2939
|
});
|
|
2044
2940
|
|
|
2045
|
-
it("
|
|
2046
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
2941
|
+
it("warns on PreToolUse git commit when mapped source changes lack staged docs refresh", async () => {
|
|
2942
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-warn-"));
|
|
2047
2943
|
try {
|
|
2944
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
2945
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
2946
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
2947
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
2948
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
2949
|
+
await writeFile(join(cwd, "README.md"), "base\n", "utf-8");
|
|
2950
|
+
execFileSync("git", ["add", "README.md", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
2951
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
2952
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
2953
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
2954
|
+
|
|
2048
2955
|
const result = await dispatchCodexNativeHook(
|
|
2049
2956
|
{
|
|
2050
|
-
hook_event_name: "
|
|
2957
|
+
hook_event_name: "PreToolUse",
|
|
2051
2958
|
cwd,
|
|
2052
2959
|
tool_name: "Bash",
|
|
2053
|
-
tool_use_id: "tool-
|
|
2054
|
-
tool_input: {
|
|
2055
|
-
|
|
2960
|
+
tool_use_id: "tool-git-commit-doc-refresh-warn",
|
|
2961
|
+
tool_input: {
|
|
2962
|
+
command: [
|
|
2963
|
+
'git commit',
|
|
2964
|
+
'-m "Keep native hooks aligned with docs"',
|
|
2965
|
+
'-m "Update the stop hook internals without refreshing the operator docs yet."',
|
|
2966
|
+
'-m "Constraint: native hook warning MVP must remain non-blocking on commit path"',
|
|
2967
|
+
'-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
|
|
2968
|
+
'-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
2969
|
+
].join(" "),
|
|
2970
|
+
},
|
|
2056
2971
|
},
|
|
2057
2972
|
{ cwd },
|
|
2058
2973
|
);
|
|
2059
2974
|
|
|
2060
|
-
assert.equal(result.omxEventName, "
|
|
2061
|
-
assert.
|
|
2975
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2976
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, undefined);
|
|
2977
|
+
assert.equal((result.outputJson as { hookSpecificOutput?: { hookEventName?: string } } | null)?.hookSpecificOutput?.hookEventName, "PreToolUse");
|
|
2978
|
+
assert.match(JSON.stringify(result.outputJson), /Document-refresh warning/);
|
|
2979
|
+
assert.match(JSON.stringify(result.outputJson), /docs\/codex-native-hooks\.md/);
|
|
2980
|
+
} finally {
|
|
2981
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
it("does not warn on PreToolUse when relevant docs are staged", async () => {
|
|
2986
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-docs-"));
|
|
2987
|
+
try {
|
|
2988
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
2989
|
+
await mkdir(join(cwd, "docs"), { recursive: true });
|
|
2990
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
2991
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
2992
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
2993
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
2994
|
+
await writeFile(join(cwd, "docs", "codex-native-hooks.md"), "initial\n", "utf-8");
|
|
2995
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts", "docs/codex-native-hooks.md"], { cwd, stdio: "ignore" });
|
|
2996
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
2997
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
2998
|
+
await writeFile(join(cwd, "docs", "codex-native-hooks.md"), "updated\n", "utf-8");
|
|
2999
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts", "docs/codex-native-hooks.md"], { cwd, stdio: "ignore" });
|
|
3000
|
+
|
|
3001
|
+
const result = await dispatchCodexNativeHook(
|
|
3002
|
+
{
|
|
3003
|
+
hook_event_name: "PreToolUse",
|
|
3004
|
+
cwd,
|
|
3005
|
+
tool_name: "Bash",
|
|
3006
|
+
tool_use_id: "tool-git-commit-doc-refresh-docs",
|
|
3007
|
+
tool_input: {
|
|
3008
|
+
command: [
|
|
3009
|
+
'git commit',
|
|
3010
|
+
'-m "Keep native hooks aligned with docs"',
|
|
3011
|
+
'-m "Update the stop hook internals and refresh the native hook docs together."',
|
|
3012
|
+
'-m "Constraint: native hook warning MVP must remain non-blocking on commit path"',
|
|
3013
|
+
'-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
|
|
3014
|
+
'-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
3015
|
+
].join(" "),
|
|
3016
|
+
},
|
|
3017
|
+
},
|
|
3018
|
+
{ cwd },
|
|
3019
|
+
);
|
|
3020
|
+
|
|
3021
|
+
assert.equal(result.outputJson, null);
|
|
3022
|
+
} finally {
|
|
3023
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
it("does not run commit-path document-refresh against payload cwd when git -C targets another repo", async () => {
|
|
3028
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-chdir-"));
|
|
3029
|
+
const otherRepo = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-other-"));
|
|
3030
|
+
try {
|
|
3031
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
3032
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
3033
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
3034
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
3035
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
3036
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
3037
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
3038
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
3039
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
3040
|
+
|
|
3041
|
+
execFileSync("git", ["init"], { cwd: otherRepo, stdio: "ignore" });
|
|
3042
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: otherRepo, stdio: "ignore" });
|
|
3043
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: otherRepo, stdio: "ignore" });
|
|
3044
|
+
await writeFile(join(otherRepo, "README.md"), "base\n", "utf-8");
|
|
3045
|
+
execFileSync("git", ["add", "README.md"], { cwd: otherRepo, stdio: "ignore" });
|
|
3046
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd: otherRepo, stdio: "ignore" });
|
|
3047
|
+
|
|
3048
|
+
const result = await dispatchCodexNativeHook(
|
|
3049
|
+
{
|
|
3050
|
+
hook_event_name: "PreToolUse",
|
|
3051
|
+
cwd,
|
|
3052
|
+
tool_name: "Bash",
|
|
3053
|
+
tool_use_id: "tool-git-commit-doc-refresh-chdir",
|
|
3054
|
+
tool_input: {
|
|
3055
|
+
command: [
|
|
3056
|
+
`git -C ${JSON.stringify(otherRepo)}`,
|
|
3057
|
+
'commit',
|
|
3058
|
+
'-m "Keep native hooks aligned with docs"',
|
|
3059
|
+
'-m "Document-refresh check should not inspect the caller cwd when commit targets another repo."',
|
|
3060
|
+
'-m "Constraint: alternate git targets are skipped unless hook-side repo resolution is added explicitly"',
|
|
3061
|
+
'-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
|
|
3062
|
+
'-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
3063
|
+
].join(" "),
|
|
3064
|
+
},
|
|
3065
|
+
},
|
|
3066
|
+
{ cwd },
|
|
3067
|
+
);
|
|
3068
|
+
|
|
3069
|
+
assert.equal(result.outputJson, null);
|
|
3070
|
+
} finally {
|
|
3071
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3072
|
+
await rm(otherRepo, { recursive: true, force: true });
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
|
|
3076
|
+
it("suppresses PreToolUse document-refresh warning when commit message includes an exemption", async () => {
|
|
3077
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-document-refresh-exempt-"));
|
|
3078
|
+
try {
|
|
3079
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
3080
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
3081
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
3082
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
3083
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
3084
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
3085
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
3086
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
3087
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
3088
|
+
|
|
3089
|
+
const result = await dispatchCodexNativeHook(
|
|
3090
|
+
{
|
|
3091
|
+
hook_event_name: "PreToolUse",
|
|
3092
|
+
cwd,
|
|
3093
|
+
tool_name: "Bash",
|
|
3094
|
+
tool_use_id: "tool-git-commit-doc-refresh-exempt",
|
|
3095
|
+
tool_input: {
|
|
3096
|
+
command: [
|
|
3097
|
+
'git commit',
|
|
3098
|
+
'-m "Keep native hooks aligned with docs"',
|
|
3099
|
+
'-m "Update the stop hook internals without docs refresh because behavior is internal-only."',
|
|
3100
|
+
'-m "Constraint: native hook warning MVP must remain non-blocking on commit path"',
|
|
3101
|
+
`-m "${DOCUMENT_REFRESH_EXEMPTION_PREFIX} internal-only behavior verified"`,
|
|
3102
|
+
'-m "Tested: node --test dist/scripts/__tests__/codex-native-hook.test.js"',
|
|
3103
|
+
'-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
|
|
3104
|
+
].join(" "),
|
|
3105
|
+
},
|
|
3106
|
+
},
|
|
3107
|
+
{ cwd },
|
|
3108
|
+
);
|
|
3109
|
+
|
|
3110
|
+
assert.equal(result.outputJson, null);
|
|
3111
|
+
} finally {
|
|
3112
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3113
|
+
}
|
|
3114
|
+
});
|
|
3115
|
+
|
|
3116
|
+
it("returns PostToolUse remediation guidance for command-not-found output", async () => {
|
|
3117
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-failure-"));
|
|
3118
|
+
try {
|
|
3119
|
+
const result = await dispatchCodexNativeHook(
|
|
3120
|
+
{
|
|
3121
|
+
hook_event_name: "PostToolUse",
|
|
3122
|
+
cwd,
|
|
3123
|
+
tool_name: "Bash",
|
|
3124
|
+
tool_use_id: "tool-fail",
|
|
3125
|
+
tool_input: { command: "foo --version" },
|
|
3126
|
+
tool_response: "{\"exit_code\":127,\"stdout\":\"\",\"stderr\":\"bash: foo: command not found\"}",
|
|
3127
|
+
},
|
|
3128
|
+
{ cwd },
|
|
3129
|
+
);
|
|
3130
|
+
|
|
3131
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3132
|
+
assert.deepEqual(result.outputJson, {
|
|
2062
3133
|
decision: "block",
|
|
2063
3134
|
reason: "The Bash output indicates a command/setup failure that should be fixed before retrying.",
|
|
2064
3135
|
hookSpecificOutput: {
|
|
@@ -2072,6 +3143,181 @@ esac
|
|
|
2072
3143
|
}
|
|
2073
3144
|
});
|
|
2074
3145
|
|
|
3146
|
+
it("stays silent when successful search output contains old Bash failure text", async () => {
|
|
3147
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-successful-search-"));
|
|
3148
|
+
try {
|
|
3149
|
+
const result = await dispatchCodexNativeHook(
|
|
3150
|
+
{
|
|
3151
|
+
hook_event_name: "PostToolUse",
|
|
3152
|
+
cwd,
|
|
3153
|
+
tool_name: "Bash",
|
|
3154
|
+
tool_use_id: "tool-search-log",
|
|
3155
|
+
tool_input: { command: "rg 'command not found' .omx/logs" },
|
|
3156
|
+
tool_response: JSON.stringify({
|
|
3157
|
+
exit_code: 0,
|
|
3158
|
+
stdout: "old-session.log: bash: foo: command not found",
|
|
3159
|
+
stderr: "",
|
|
3160
|
+
}),
|
|
3161
|
+
},
|
|
3162
|
+
{ cwd },
|
|
3163
|
+
);
|
|
3164
|
+
|
|
3165
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3166
|
+
assert.equal(result.outputJson, null);
|
|
3167
|
+
} finally {
|
|
3168
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
it("stays silent when Bash stdout only contains failure-like source text", async () => {
|
|
3173
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-failure-source-text-"));
|
|
3174
|
+
try {
|
|
3175
|
+
const result = await dispatchCodexNativeHook(
|
|
3176
|
+
{
|
|
3177
|
+
hook_event_name: "PostToolUse",
|
|
3178
|
+
cwd,
|
|
3179
|
+
tool_name: "Bash",
|
|
3180
|
+
tool_use_id: "tool-source-text",
|
|
3181
|
+
tool_input: { command: "sed -n '1,40p' hook-source.ts" },
|
|
3182
|
+
tool_response: "const text = 'bash: foo: command not found';\nconst detail = 'permission denied';",
|
|
3183
|
+
},
|
|
3184
|
+
{ cwd },
|
|
3185
|
+
);
|
|
3186
|
+
|
|
3187
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3188
|
+
assert.equal(result.outputJson, null);
|
|
3189
|
+
} finally {
|
|
3190
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
it("stays silent for rc-zero build logs that mention missing grep paths", async () => {
|
|
3195
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-build-log-"));
|
|
3196
|
+
try {
|
|
3197
|
+
const result = await dispatchCodexNativeHook(
|
|
3198
|
+
{
|
|
3199
|
+
hook_event_name: "PostToolUse",
|
|
3200
|
+
cwd,
|
|
3201
|
+
tool_name: "Bash",
|
|
3202
|
+
tool_use_id: "tool-build-log",
|
|
3203
|
+
tool_input: { command: "npm run build" },
|
|
3204
|
+
tool_response: JSON.stringify({
|
|
3205
|
+
exit_code: 0,
|
|
3206
|
+
stdout: "build passed\nnote: grep fixture says no such file or directory",
|
|
3207
|
+
stderr: "",
|
|
3208
|
+
}),
|
|
3209
|
+
},
|
|
3210
|
+
{ cwd },
|
|
3211
|
+
);
|
|
3212
|
+
|
|
3213
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3214
|
+
assert.equal(result.outputJson, null);
|
|
3215
|
+
} finally {
|
|
3216
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
|
|
3220
|
+
it("does not treat Bash output containing MCP transport text as MCP transport death", async () => {
|
|
3221
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-source-text-"));
|
|
3222
|
+
try {
|
|
3223
|
+
const result = await dispatchCodexNativeHook(
|
|
3224
|
+
{
|
|
3225
|
+
hook_event_name: "PostToolUse",
|
|
3226
|
+
cwd,
|
|
3227
|
+
tool_name: "Bash",
|
|
3228
|
+
tool_use_id: "tool-mcp-source-text",
|
|
3229
|
+
tool_input: { command: "sed -n '580,620p' codex-native-pre-post.ts" },
|
|
3230
|
+
tool_response: JSON.stringify({
|
|
3231
|
+
exit_code: 0,
|
|
3232
|
+
stdout: "reason: 'MCP transport closed before response over stdio pipe closed'",
|
|
3233
|
+
stderr: "",
|
|
3234
|
+
}),
|
|
3235
|
+
},
|
|
3236
|
+
{ cwd },
|
|
3237
|
+
);
|
|
3238
|
+
|
|
3239
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3240
|
+
assert.equal(result.outputJson, null);
|
|
3241
|
+
} finally {
|
|
3242
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
|
|
3246
|
+
it("stays silent when successful output includes prior hook context text", async () => {
|
|
3247
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-recursive-context-"));
|
|
3248
|
+
try {
|
|
3249
|
+
const result = await dispatchCodexNativeHook(
|
|
3250
|
+
{
|
|
3251
|
+
hook_event_name: "PostToolUse",
|
|
3252
|
+
cwd,
|
|
3253
|
+
tool_name: "Bash",
|
|
3254
|
+
tool_use_id: "tool-hook-context",
|
|
3255
|
+
tool_input: { command: "cat transcript.txt" },
|
|
3256
|
+
tool_response: JSON.stringify({
|
|
3257
|
+
exit_code: 0,
|
|
3258
|
+
stdout:
|
|
3259
|
+
"Bash reported `command not found`, `permission denied`, or a missing file/path. Verify the command, dependency installation, PATH, file permissions, and referenced paths before retrying.",
|
|
3260
|
+
stderr: "",
|
|
3261
|
+
}),
|
|
3262
|
+
},
|
|
3263
|
+
{ cwd },
|
|
3264
|
+
);
|
|
3265
|
+
|
|
3266
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3267
|
+
assert.equal(result.outputJson, null);
|
|
3268
|
+
} finally {
|
|
3269
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
|
|
3273
|
+
it("stays silent when successful Bash output quotes MCP transport warnings", async () => {
|
|
3274
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-bash-mcp-quote-"));
|
|
3275
|
+
try {
|
|
3276
|
+
const result = await dispatchCodexNativeHook(
|
|
3277
|
+
{
|
|
3278
|
+
hook_event_name: "PostToolUse",
|
|
3279
|
+
cwd,
|
|
3280
|
+
tool_name: "Bash",
|
|
3281
|
+
tool_use_id: "tool-bash-mcp-quote",
|
|
3282
|
+
tool_input: { command: "cat diagnostic-log.txt" },
|
|
3283
|
+
tool_response: JSON.stringify({
|
|
3284
|
+
exit_code: 0,
|
|
3285
|
+
stdout: "diagnostic log quoted: MCP transport closed; stdio pipe closed before response",
|
|
3286
|
+
stderr: "",
|
|
3287
|
+
}),
|
|
3288
|
+
},
|
|
3289
|
+
{ cwd },
|
|
3290
|
+
);
|
|
3291
|
+
|
|
3292
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3293
|
+
assert.equal(result.outputJson, null);
|
|
3294
|
+
} finally {
|
|
3295
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3296
|
+
}
|
|
3297
|
+
});
|
|
3298
|
+
|
|
3299
|
+
it("stays silent when Bash hard-failure text has no parsed exit code", async () => {
|
|
3300
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-bash-unparsed-failure-"));
|
|
3301
|
+
try {
|
|
3302
|
+
const result = await dispatchCodexNativeHook(
|
|
3303
|
+
{
|
|
3304
|
+
hook_event_name: "PostToolUse",
|
|
3305
|
+
cwd,
|
|
3306
|
+
tool_name: "Bash",
|
|
3307
|
+
tool_use_id: "tool-bash-unparsed-failure",
|
|
3308
|
+
tool_input: { command: "cat captured-output.txt" },
|
|
3309
|
+
tool_response: "captured transcript says: bash: foo: command not found",
|
|
3310
|
+
},
|
|
3311
|
+
{ cwd },
|
|
3312
|
+
);
|
|
3313
|
+
|
|
3314
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
3315
|
+
assert.equal(result.outputJson, null);
|
|
3316
|
+
} finally {
|
|
3317
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3318
|
+
}
|
|
3319
|
+
});
|
|
3320
|
+
|
|
2075
3321
|
it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
|
|
2076
3322
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
|
|
2077
3323
|
try {
|
|
@@ -2688,7 +3934,17 @@ esac
|
|
|
2688
3934
|
undefined,
|
|
2689
3935
|
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker" },
|
|
2690
3936
|
);
|
|
3937
|
+
const workerCwd = join(cwd, ".omx", "team", "worker-stop-team", "worktrees", "worker-1");
|
|
2691
3938
|
const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team", "workers", "worker-1");
|
|
3939
|
+
await mkdir(workerCwd, { recursive: true });
|
|
3940
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
3941
|
+
name: "worker-1",
|
|
3942
|
+
index: 1,
|
|
3943
|
+
role: "executor",
|
|
3944
|
+
assigned_tasks: ["1"],
|
|
3945
|
+
worktree_path: workerCwd,
|
|
3946
|
+
team_state_root: join(cwd, ".omx", "state"),
|
|
3947
|
+
});
|
|
2692
3948
|
await writeJson(join(workerDir, "status.json"), {
|
|
2693
3949
|
state: "idle",
|
|
2694
3950
|
current_task_id: "1",
|
|
@@ -2710,10 +3966,10 @@ esac
|
|
|
2710
3966
|
const result = await dispatchCodexNativeHook(
|
|
2711
3967
|
{
|
|
2712
3968
|
hook_event_name: "Stop",
|
|
2713
|
-
cwd:
|
|
3969
|
+
cwd: workerCwd,
|
|
2714
3970
|
session_id: "sess-stop-team-worker",
|
|
2715
3971
|
},
|
|
2716
|
-
{ cwd:
|
|
3972
|
+
{ cwd: workerCwd },
|
|
2717
3973
|
);
|
|
2718
3974
|
|
|
2719
3975
|
assert.deepEqual(result.outputJson, {
|
|
@@ -2734,51 +3990,148 @@ esac
|
|
|
2734
3990
|
}
|
|
2735
3991
|
});
|
|
2736
3992
|
|
|
2737
|
-
it("
|
|
2738
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-
|
|
2739
|
-
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
2740
|
-
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
3993
|
+
it("suppresses identical team worker Stop replays but re-blocks fresh turns and task state changes", async () => {
|
|
3994
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
|
|
2741
3995
|
try {
|
|
2742
3996
|
await initTeamState(
|
|
2743
|
-
"worker-
|
|
2744
|
-
"worker stop
|
|
3997
|
+
"worker-repeat-team",
|
|
3998
|
+
"worker stop repeat guard",
|
|
2745
3999
|
"executor",
|
|
2746
4000
|
1,
|
|
2747
4001
|
cwd,
|
|
2748
4002
|
undefined,
|
|
2749
|
-
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-
|
|
4003
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-repeat" },
|
|
2750
4004
|
);
|
|
2751
|
-
const
|
|
4005
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4006
|
+
const workerDir = join(stateDir, "team", "worker-repeat-team", "workers", "worker-1");
|
|
4007
|
+
const taskPath = join(stateDir, "team", "worker-repeat-team", "tasks", "task-1.json");
|
|
4008
|
+
const workerCwd = join(cwd, ".omx", "team", "worker-repeat-team", "worktrees", "worker-1");
|
|
4009
|
+
await mkdir(workerCwd, { recursive: true });
|
|
4010
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4011
|
+
name: "worker-1",
|
|
4012
|
+
index: 1,
|
|
4013
|
+
role: "executor",
|
|
4014
|
+
assigned_tasks: ["1"],
|
|
4015
|
+
worktree_path: workerCwd,
|
|
4016
|
+
team_state_root: stateDir,
|
|
4017
|
+
});
|
|
2752
4018
|
await writeJson(join(workerDir, "status.json"), {
|
|
2753
|
-
state: "
|
|
4019
|
+
state: "idle",
|
|
2754
4020
|
current_task_id: "1",
|
|
2755
4021
|
updated_at: new Date().toISOString(),
|
|
2756
4022
|
});
|
|
2757
|
-
await writeJson(
|
|
4023
|
+
await writeJson(taskPath, {
|
|
2758
4024
|
id: "1",
|
|
2759
4025
|
subject: "hook task",
|
|
2760
4026
|
description: "finish hook task",
|
|
2761
|
-
status: "
|
|
4027
|
+
status: "in_progress",
|
|
2762
4028
|
owner: "worker-1",
|
|
2763
4029
|
created_at: new Date().toISOString(),
|
|
2764
4030
|
});
|
|
2765
4031
|
|
|
2766
|
-
process.env.OMX_TEAM_WORKER = "worker-
|
|
2767
|
-
process.env.OMX_TEAM_STATE_ROOT =
|
|
2768
|
-
|
|
2769
|
-
const result = await dispatchCodexNativeHook(
|
|
2770
|
-
{
|
|
2771
|
-
hook_event_name: "Stop",
|
|
2772
|
-
cwd,
|
|
2773
|
-
session_id: "sess-stop-team-worker-terminal",
|
|
2774
|
-
},
|
|
2775
|
-
{ cwd },
|
|
2776
|
-
);
|
|
4032
|
+
process.env.OMX_TEAM_WORKER = "worker-repeat-team/worker-1";
|
|
4033
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4034
|
+
process.env.OMX_TEAM_LEADER_CWD = cwd;
|
|
2777
4035
|
|
|
2778
|
-
|
|
4036
|
+
const basePayload = {
|
|
4037
|
+
hook_event_name: "Stop",
|
|
4038
|
+
cwd: workerCwd,
|
|
4039
|
+
session_id: "sess-stop-team-worker-repeat",
|
|
4040
|
+
thread_id: "thread-stop-team-worker-repeat",
|
|
4041
|
+
turn_id: "turn-stop-team-worker-repeat-1",
|
|
4042
|
+
last_assistant_message: "I need to stop before this task is done.",
|
|
4043
|
+
};
|
|
4044
|
+
const expectedInProgress = {
|
|
2779
4045
|
decision: "block",
|
|
2780
4046
|
reason:
|
|
2781
|
-
|
|
4047
|
+
"OMX team worker worker-1 is still assigned non-terminal task 1 (in_progress); continue the current assigned task or report a concrete blocker before stopping.",
|
|
4048
|
+
stopReason: "team_worker_worker-1_1_in_progress",
|
|
4049
|
+
systemMessage: "OMX team worker worker-1 is still assigned task 1 (in_progress).",
|
|
4050
|
+
};
|
|
4051
|
+
|
|
4052
|
+
const first = await dispatchCodexNativeHook(basePayload, { cwd: workerCwd });
|
|
4053
|
+
const replay = await dispatchCodexNativeHook(
|
|
4054
|
+
{ ...basePayload, stop_hook_active: true },
|
|
4055
|
+
{ cwd: workerCwd },
|
|
4056
|
+
);
|
|
4057
|
+
const freshTurn = await dispatchCodexNativeHook(
|
|
4058
|
+
{ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true },
|
|
4059
|
+
{ cwd: workerCwd },
|
|
4060
|
+
);
|
|
4061
|
+
|
|
4062
|
+
await writeJson(taskPath, {
|
|
4063
|
+
id: "1",
|
|
4064
|
+
subject: "hook task",
|
|
4065
|
+
description: "finish hook task",
|
|
4066
|
+
status: "blocked",
|
|
4067
|
+
owner: "worker-1",
|
|
4068
|
+
created_at: new Date().toISOString(),
|
|
4069
|
+
});
|
|
4070
|
+
const stateChanged = await dispatchCodexNativeHook(
|
|
4071
|
+
{ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true },
|
|
4072
|
+
{ cwd: workerCwd },
|
|
4073
|
+
);
|
|
4074
|
+
|
|
4075
|
+
assert.deepEqual(first.outputJson, expectedInProgress);
|
|
4076
|
+
assert.deepEqual(replay.outputJson, null);
|
|
4077
|
+
assert.deepEqual(freshTurn.outputJson, expectedInProgress);
|
|
4078
|
+
assert.deepEqual(stateChanged.outputJson, {
|
|
4079
|
+
decision: "block",
|
|
4080
|
+
reason:
|
|
4081
|
+
"OMX team worker worker-1 is still assigned non-terminal task 1 (blocked); continue the current assigned task or report a concrete blocker before stopping.",
|
|
4082
|
+
stopReason: "team_worker_worker-1_1_blocked",
|
|
4083
|
+
systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
|
|
4084
|
+
});
|
|
4085
|
+
} finally {
|
|
4086
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
|
|
4090
|
+
it("does not block Stop for a team worker when assigned task is terminal", async () => {
|
|
4091
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
|
|
4092
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4093
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4094
|
+
try {
|
|
4095
|
+
await initTeamState(
|
|
4096
|
+
"worker-stop-team-terminal",
|
|
4097
|
+
"worker stop terminal fallback",
|
|
4098
|
+
"executor",
|
|
4099
|
+
1,
|
|
4100
|
+
cwd,
|
|
4101
|
+
undefined,
|
|
4102
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" },
|
|
4103
|
+
);
|
|
4104
|
+
const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
|
|
4105
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
4106
|
+
state: "done",
|
|
4107
|
+
current_task_id: "1",
|
|
4108
|
+
updated_at: new Date().toISOString(),
|
|
4109
|
+
});
|
|
4110
|
+
await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "tasks", "task-1.json"), {
|
|
4111
|
+
id: "1",
|
|
4112
|
+
subject: "hook task",
|
|
4113
|
+
description: "finish hook task",
|
|
4114
|
+
status: "completed",
|
|
4115
|
+
owner: "worker-1",
|
|
4116
|
+
created_at: new Date().toISOString(),
|
|
4117
|
+
});
|
|
4118
|
+
|
|
4119
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
|
|
4120
|
+
process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
|
|
4121
|
+
|
|
4122
|
+
const result = await dispatchCodexNativeHook(
|
|
4123
|
+
{
|
|
4124
|
+
hook_event_name: "Stop",
|
|
4125
|
+
cwd,
|
|
4126
|
+
session_id: "sess-stop-team-worker-terminal",
|
|
4127
|
+
},
|
|
4128
|
+
{ cwd },
|
|
4129
|
+
);
|
|
4130
|
+
|
|
4131
|
+
assert.deepEqual(result.outputJson, {
|
|
4132
|
+
decision: "block",
|
|
4133
|
+
reason:
|
|
4134
|
+
`OMX team pipeline is still active (worker-stop-team-terminal) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
2782
4135
|
stopReason: "team_team-exec",
|
|
2783
4136
|
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
2784
4137
|
});
|
|
@@ -2921,6 +4274,51 @@ esac
|
|
|
2921
4274
|
}
|
|
2922
4275
|
});
|
|
2923
4276
|
|
|
4277
|
+
it("honors terminal team run-state before later canonical-team Stop fallback", async () => {
|
|
4278
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-terminal-run-state-canonical-"));
|
|
4279
|
+
try {
|
|
4280
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4281
|
+
const sessionId = "sess-stop-team-terminal-run-state";
|
|
4282
|
+
await initTeamState(
|
|
4283
|
+
"terminal-run-state-team",
|
|
4284
|
+
"terminal team stop canonical fallback regression",
|
|
4285
|
+
"executor",
|
|
4286
|
+
1,
|
|
4287
|
+
cwd,
|
|
4288
|
+
undefined,
|
|
4289
|
+
{ ...process.env, OMX_SESSION_ID: sessionId },
|
|
4290
|
+
);
|
|
4291
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
4292
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
4293
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
4294
|
+
version: 1,
|
|
4295
|
+
mode: "team",
|
|
4296
|
+
active: false,
|
|
4297
|
+
outcome: "finish",
|
|
4298
|
+
lifecycle_outcome: "finished",
|
|
4299
|
+
current_phase: "complete",
|
|
4300
|
+
completed_at: "2026-04-27T12:00:00.000Z",
|
|
4301
|
+
updated_at: "2026-04-27T12:00:00.000Z",
|
|
4302
|
+
});
|
|
4303
|
+
|
|
4304
|
+
const result = await dispatchCodexNativeHook(
|
|
4305
|
+
{
|
|
4306
|
+
hook_event_name: "Stop",
|
|
4307
|
+
cwd,
|
|
4308
|
+
session_id: sessionId,
|
|
4309
|
+
thread_id: "thread-stop-team-terminal-run-state",
|
|
4310
|
+
turn_id: "turn-stop-team-terminal-run-state-1",
|
|
4311
|
+
},
|
|
4312
|
+
{ cwd },
|
|
4313
|
+
);
|
|
4314
|
+
|
|
4315
|
+
assert.equal(result.omxEventName, "stop");
|
|
4316
|
+
assert.equal(result.outputJson, null);
|
|
4317
|
+
} finally {
|
|
4318
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4319
|
+
}
|
|
4320
|
+
});
|
|
4321
|
+
|
|
2924
4322
|
it("re-fires canonical-team Stop output for a later fresh Stop reply when coarse mode state is missing", async () => {
|
|
2925
4323
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-canonical-refire-"));
|
|
2926
4324
|
try {
|
|
@@ -3883,6 +5281,123 @@ esac
|
|
|
3883
5281
|
}
|
|
3884
5282
|
});
|
|
3885
5283
|
|
|
5284
|
+
it("returns a non-blocking Stop document-refresh warning before auto-nudge when Ralph is not active", async () => {
|
|
5285
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-"));
|
|
5286
|
+
try {
|
|
5287
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
5288
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
5289
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
5290
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
5291
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
5292
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
5293
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
5294
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
5295
|
+
|
|
5296
|
+
const result = await dispatchCodexNativeHook(
|
|
5297
|
+
{
|
|
5298
|
+
hook_event_name: "Stop",
|
|
5299
|
+
cwd,
|
|
5300
|
+
session_id: "sess-stop-doc-refresh",
|
|
5301
|
+
last_assistant_message: "Launch-ready: yes",
|
|
5302
|
+
},
|
|
5303
|
+
{ cwd },
|
|
5304
|
+
);
|
|
5305
|
+
|
|
5306
|
+
assert.equal(result.omxEventName, "stop");
|
|
5307
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, undefined);
|
|
5308
|
+
assert.equal((result.outputJson as { hookSpecificOutput?: { hookEventName?: string } } | null)?.hookSpecificOutput?.hookEventName, "Stop");
|
|
5309
|
+
assert.match(JSON.stringify(result.outputJson), /Document-refresh warning/);
|
|
5310
|
+
assert.match(JSON.stringify(result.outputJson), /staged \+ unstaged changes/);
|
|
5311
|
+
} finally {
|
|
5312
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5313
|
+
}
|
|
5314
|
+
});
|
|
5315
|
+
|
|
5316
|
+
it("does not warn on ordinary non-terminal Stop attempts before auto-nudge", async () => {
|
|
5317
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-nonterminal-"));
|
|
5318
|
+
try {
|
|
5319
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
5320
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
5321
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
5322
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
5323
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
5324
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
5325
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
5326
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
5327
|
+
|
|
5328
|
+
const result = await dispatchCodexNativeHook(
|
|
5329
|
+
{
|
|
5330
|
+
hook_event_name: "Stop",
|
|
5331
|
+
cwd,
|
|
5332
|
+
session_id: "sess-stop-doc-refresh-nonterminal",
|
|
5333
|
+
last_assistant_message: "Continuing implementation; next I will run focused tests.",
|
|
5334
|
+
},
|
|
5335
|
+
{ cwd },
|
|
5336
|
+
);
|
|
5337
|
+
|
|
5338
|
+
assert.equal(result.outputJson, null);
|
|
5339
|
+
} finally {
|
|
5340
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5341
|
+
}
|
|
5342
|
+
});
|
|
5343
|
+
|
|
5344
|
+
it("dedupes identical Stop document-refresh warnings during active Stop-hook replays", async () => {
|
|
5345
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-dedupe-"));
|
|
5346
|
+
try {
|
|
5347
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
5348
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
5349
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
5350
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
5351
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
5352
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
5353
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
5354
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
5355
|
+
|
|
5356
|
+
const payload = {
|
|
5357
|
+
hook_event_name: "Stop",
|
|
5358
|
+
cwd,
|
|
5359
|
+
session_id: "sess-stop-doc-refresh-dedupe",
|
|
5360
|
+
last_assistant_message: "Launch-ready: yes",
|
|
5361
|
+
} as const;
|
|
5362
|
+
|
|
5363
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
5364
|
+
const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
|
|
5365
|
+
|
|
5366
|
+
assert.match(JSON.stringify(first.outputJson), /Document-refresh warning/);
|
|
5367
|
+
assert.equal(replay.outputJson, null);
|
|
5368
|
+
} finally {
|
|
5369
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5370
|
+
}
|
|
5371
|
+
});
|
|
5372
|
+
|
|
5373
|
+
it("suppresses Stop document-refresh warning when the final handoff message includes an exemption", async () => {
|
|
5374
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-document-refresh-exempt-"));
|
|
5375
|
+
try {
|
|
5376
|
+
await mkdir(join(cwd, "src", "scripts"), { recursive: true });
|
|
5377
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
5378
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
5379
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
5380
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 1;\n", "utf-8");
|
|
5381
|
+
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
5382
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
5383
|
+
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
5384
|
+
|
|
5385
|
+
const result = await dispatchCodexNativeHook(
|
|
5386
|
+
{
|
|
5387
|
+
hook_event_name: "Stop",
|
|
5388
|
+
cwd,
|
|
5389
|
+
session_id: "sess-stop-doc-refresh-exempt",
|
|
5390
|
+
last_assistant_message: `${DOCUMENT_REFRESH_EXEMPTION_PREFIX} internal-only behavior verified`,
|
|
5391
|
+
},
|
|
5392
|
+
{ cwd },
|
|
5393
|
+
);
|
|
5394
|
+
|
|
5395
|
+
assert.equal(result.outputJson, null);
|
|
5396
|
+
} finally {
|
|
5397
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5398
|
+
}
|
|
5399
|
+
});
|
|
5400
|
+
|
|
3886
5401
|
it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
|
|
3887
5402
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
|
|
3888
5403
|
try {
|
|
@@ -3908,10 +5423,10 @@ esac
|
|
|
3908
5423
|
assert.deepEqual(result.outputJson, {
|
|
3909
5424
|
decision: "block",
|
|
3910
5425
|
reason:
|
|
3911
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
5426
|
+
"OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
3912
5427
|
stopReason: "ralph_executing",
|
|
3913
5428
|
systemMessage:
|
|
3914
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
5429
|
+
"OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
3915
5430
|
});
|
|
3916
5431
|
} finally {
|
|
3917
5432
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -3943,10 +5458,10 @@ esac
|
|
|
3943
5458
|
assert.deepEqual(result.outputJson, {
|
|
3944
5459
|
decision: "block",
|
|
3945
5460
|
reason:
|
|
3946
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
5461
|
+
"OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-live-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
3947
5462
|
stopReason: "ralph_executing",
|
|
3948
5463
|
systemMessage:
|
|
3949
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
5464
|
+
"OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-live-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
3950
5465
|
});
|
|
3951
5466
|
} finally {
|
|
3952
5467
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -4031,6 +5546,87 @@ esac
|
|
|
4031
5546
|
}
|
|
4032
5547
|
});
|
|
4033
5548
|
|
|
5549
|
+
it("does not hard-block Stop on stale session-scoped Ralph starting state after visible active modes are cleared", async () => {
|
|
5550
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-cleared-stale-ralph-"));
|
|
5551
|
+
try {
|
|
5552
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5553
|
+
const sessionId = "sess-cleared-ralph";
|
|
5554
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5555
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
5556
|
+
active: true,
|
|
5557
|
+
mode: "ralph",
|
|
5558
|
+
current_phase: "starting",
|
|
5559
|
+
session_id: sessionId,
|
|
5560
|
+
});
|
|
5561
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
5562
|
+
active: false,
|
|
5563
|
+
skill: "ralph",
|
|
5564
|
+
active_skills: [],
|
|
5565
|
+
});
|
|
5566
|
+
|
|
5567
|
+
const listActive = await executeStateOperation("state_list_active", {
|
|
5568
|
+
workingDirectory: cwd,
|
|
5569
|
+
});
|
|
5570
|
+
assert.deepEqual(listActive.payload, { active_modes: [] });
|
|
5571
|
+
|
|
5572
|
+
const result = await dispatchCodexNativeHook(
|
|
5573
|
+
{
|
|
5574
|
+
hook_event_name: "Stop",
|
|
5575
|
+
cwd,
|
|
5576
|
+
session_id: sessionId,
|
|
5577
|
+
},
|
|
5578
|
+
{ cwd },
|
|
5579
|
+
);
|
|
5580
|
+
|
|
5581
|
+
assert.equal(result.omxEventName, "stop");
|
|
5582
|
+
assert.equal(result.outputJson, null);
|
|
5583
|
+
} finally {
|
|
5584
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5585
|
+
}
|
|
5586
|
+
});
|
|
5587
|
+
|
|
5588
|
+
it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
|
|
5589
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
|
|
5590
|
+
try {
|
|
5591
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5592
|
+
const sessionId = "sess-visible-ralph";
|
|
5593
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5594
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
5595
|
+
active: true,
|
|
5596
|
+
mode: "ralph",
|
|
5597
|
+
current_phase: "starting",
|
|
5598
|
+
session_id: sessionId,
|
|
5599
|
+
});
|
|
5600
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
5601
|
+
active: true,
|
|
5602
|
+
skill: "ralph",
|
|
5603
|
+
phase: "starting",
|
|
5604
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
|
|
5605
|
+
});
|
|
5606
|
+
|
|
5607
|
+
const result = await dispatchCodexNativeHook(
|
|
5608
|
+
{
|
|
5609
|
+
hook_event_name: "Stop",
|
|
5610
|
+
cwd,
|
|
5611
|
+
session_id: sessionId,
|
|
5612
|
+
},
|
|
5613
|
+
{ cwd },
|
|
5614
|
+
);
|
|
5615
|
+
|
|
5616
|
+
assert.equal(result.omxEventName, "stop");
|
|
5617
|
+
assert.deepEqual(result.outputJson, {
|
|
5618
|
+
decision: "block",
|
|
5619
|
+
reason:
|
|
5620
|
+
"OMX Ralph is still active (phase: starting; state: .omx/state/sessions/sess-visible-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
5621
|
+
stopReason: "ralph_starting",
|
|
5622
|
+
systemMessage:
|
|
5623
|
+
"OMX Ralph is still active (phase: starting; state: .omx/state/sessions/sess-visible-ralph/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
5624
|
+
});
|
|
5625
|
+
} finally {
|
|
5626
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5627
|
+
}
|
|
5628
|
+
});
|
|
5629
|
+
|
|
4034
5630
|
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
4035
5631
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
4036
5632
|
try {
|
|
@@ -4058,6 +5654,46 @@ esac
|
|
|
4058
5654
|
}
|
|
4059
5655
|
});
|
|
4060
5656
|
|
|
5657
|
+
it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
|
|
5658
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
|
|
5659
|
+
try {
|
|
5660
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5661
|
+
const sessionId = "sess-canonical-run-state-ralph";
|
|
5662
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5663
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
5664
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
5665
|
+
version: 1,
|
|
5666
|
+
mode: "ralph",
|
|
5667
|
+
active: false,
|
|
5668
|
+
outcome: "finish",
|
|
5669
|
+
lifecycle_outcome: "finished",
|
|
5670
|
+
current_phase: "complete",
|
|
5671
|
+
completed_at: "2026-04-27T12:00:00.000Z",
|
|
5672
|
+
updated_at: "2026-04-27T12:00:00.000Z",
|
|
5673
|
+
});
|
|
5674
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
5675
|
+
active: true,
|
|
5676
|
+
mode: "ralph",
|
|
5677
|
+
current_phase: "verifying",
|
|
5678
|
+
session_id: sessionId,
|
|
5679
|
+
});
|
|
5680
|
+
|
|
5681
|
+
const result = await dispatchCodexNativeHook(
|
|
5682
|
+
{
|
|
5683
|
+
hook_event_name: "Stop",
|
|
5684
|
+
cwd,
|
|
5685
|
+
session_id: sessionId,
|
|
5686
|
+
},
|
|
5687
|
+
{ cwd },
|
|
5688
|
+
);
|
|
5689
|
+
|
|
5690
|
+
assert.equal(result.omxEventName, "stop");
|
|
5691
|
+
assert.equal(result.outputJson, null);
|
|
5692
|
+
} finally {
|
|
5693
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5694
|
+
}
|
|
5695
|
+
});
|
|
5696
|
+
|
|
4061
5697
|
it("does not block Stop from root Ralph fallback when the current session has no scoped Ralph state", async () => {
|
|
4062
5698
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-fallback-ralph-"));
|
|
4063
5699
|
try {
|
|
@@ -4171,10 +5807,10 @@ esac
|
|
|
4171
5807
|
const expected = {
|
|
4172
5808
|
decision: "block",
|
|
4173
5809
|
reason:
|
|
4174
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
5810
|
+
"OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
4175
5811
|
stopReason: "ralph_executing",
|
|
4176
5812
|
systemMessage:
|
|
4177
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
5813
|
+
"OMX Ralph is still active (phase: executing; state: .omx/state/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
4178
5814
|
};
|
|
4179
5815
|
|
|
4180
5816
|
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
@@ -5566,6 +7202,452 @@ describe("codex native hook triage integration", () => {
|
|
|
5566
7202
|
}
|
|
5567
7203
|
});
|
|
5568
7204
|
|
|
7205
|
+
it("injects LIGHT/researcher advisory and writes state for an official-doc lookup prompt", async () => {
|
|
7206
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-researcher-"));
|
|
7207
|
+
try {
|
|
7208
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7209
|
+
const result = await dispatchCodexNativeHook(
|
|
7210
|
+
{
|
|
7211
|
+
hook_event_name: "UserPromptSubmit",
|
|
7212
|
+
cwd,
|
|
7213
|
+
session_id: "triage-researcher-1",
|
|
7214
|
+
thread_id: "thread-triage-researcher-1",
|
|
7215
|
+
turn_id: "turn-triage-researcher-1",
|
|
7216
|
+
prompt: "Find the official docs and version compatibility notes for this SDK",
|
|
7217
|
+
},
|
|
7218
|
+
{ cwd },
|
|
7219
|
+
);
|
|
7220
|
+
|
|
7221
|
+
const additionalContext = String(
|
|
7222
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7223
|
+
);
|
|
7224
|
+
assert.match(additionalContext, /external documentation\/reference research request/);
|
|
7225
|
+
assert.match(additionalContext, /Prefer the researcher role surface/);
|
|
7226
|
+
assert.doesNotMatch(additionalContext, /skill: researcher activated/);
|
|
7227
|
+
|
|
7228
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
7229
|
+
|
|
7230
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-1", "prompt-routing-state.json");
|
|
7231
|
+
assert.equal(existsSync(stateFile), true);
|
|
7232
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7233
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7234
|
+
suppress_followup?: boolean;
|
|
7235
|
+
};
|
|
7236
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7237
|
+
assert.equal(state.last_triage?.destination, "researcher");
|
|
7238
|
+
assert.equal(state.last_triage?.reason, "external_reference_research");
|
|
7239
|
+
assert.equal(state.suppress_followup, true);
|
|
7240
|
+
} finally {
|
|
7241
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7242
|
+
}
|
|
7243
|
+
});
|
|
7244
|
+
|
|
7245
|
+
it("routes Korean external lookup phrasing to researcher without treating it as workflow activation", async () => {
|
|
7246
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-researcher-ko-"));
|
|
7247
|
+
try {
|
|
7248
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7249
|
+
const result = await dispatchCodexNativeHook(
|
|
7250
|
+
{
|
|
7251
|
+
hook_event_name: "UserPromptSubmit",
|
|
7252
|
+
cwd,
|
|
7253
|
+
session_id: "triage-researcher-ko-1",
|
|
7254
|
+
thread_id: "thread-triage-researcher-ko-1",
|
|
7255
|
+
turn_id: "turn-triage-researcher-ko-1",
|
|
7256
|
+
prompt: "OpenAI Responses API 공식 문서 찾아줘",
|
|
7257
|
+
},
|
|
7258
|
+
{ cwd },
|
|
7259
|
+
);
|
|
7260
|
+
|
|
7261
|
+
const additionalContext = String(
|
|
7262
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7263
|
+
);
|
|
7264
|
+
assert.match(additionalContext, /Prefer the researcher role surface/);
|
|
7265
|
+
assert.equal(result.skillState, null);
|
|
7266
|
+
|
|
7267
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-ko-1", "prompt-routing-state.json");
|
|
7268
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7269
|
+
last_triage?: { lane?: string; destination?: string };
|
|
7270
|
+
};
|
|
7271
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7272
|
+
assert.equal(state.last_triage?.destination, "researcher");
|
|
7273
|
+
} finally {
|
|
7274
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7275
|
+
}
|
|
7276
|
+
});
|
|
7277
|
+
|
|
7278
|
+
it("routes official-doc question prompts to researcher instead of explore", async () => {
|
|
7279
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-question-researcher-"));
|
|
7280
|
+
try {
|
|
7281
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7282
|
+
const result = await dispatchCodexNativeHook(
|
|
7283
|
+
{
|
|
7284
|
+
hook_event_name: "UserPromptSubmit",
|
|
7285
|
+
cwd,
|
|
7286
|
+
session_id: "triage-question-researcher-1",
|
|
7287
|
+
thread_id: "thread-triage-question-researcher-1",
|
|
7288
|
+
turn_id: "turn-triage-question-researcher-1",
|
|
7289
|
+
prompt: "where can I find official docs for OpenAI Responses API?",
|
|
7290
|
+
},
|
|
7291
|
+
{ cwd },
|
|
7292
|
+
);
|
|
7293
|
+
|
|
7294
|
+
const additionalContext = String(
|
|
7295
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7296
|
+
);
|
|
7297
|
+
assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
|
|
7298
|
+
assert.match(additionalContext, /Prefer the researcher role surface/);
|
|
7299
|
+
|
|
7300
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-question-researcher-1", "prompt-routing-state.json");
|
|
7301
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7302
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7303
|
+
};
|
|
7304
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7305
|
+
assert.equal(state.last_triage?.destination, "researcher");
|
|
7306
|
+
assert.equal(state.last_triage?.reason, "external_reference_research");
|
|
7307
|
+
} finally {
|
|
7308
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7309
|
+
}
|
|
7310
|
+
});
|
|
7311
|
+
|
|
7312
|
+
it("routes endpoint-shaped official-doc lookups to researcher instead of local explore", async () => {
|
|
7313
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-endpoint-researcher-"));
|
|
7314
|
+
try {
|
|
7315
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7316
|
+
const result = await dispatchCodexNativeHook(
|
|
7317
|
+
{
|
|
7318
|
+
hook_event_name: "UserPromptSubmit",
|
|
7319
|
+
cwd,
|
|
7320
|
+
session_id: "triage-endpoint-researcher-1",
|
|
7321
|
+
thread_id: "thread-triage-endpoint-researcher-1",
|
|
7322
|
+
turn_id: "turn-triage-endpoint-researcher-1",
|
|
7323
|
+
prompt: "find official docs for api/v1/responses",
|
|
7324
|
+
},
|
|
7325
|
+
{ cwd },
|
|
7326
|
+
);
|
|
7327
|
+
|
|
7328
|
+
const additionalContext = String(
|
|
7329
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7330
|
+
);
|
|
7331
|
+
assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
|
|
7332
|
+
assert.match(additionalContext, /Prefer the researcher role surface/);
|
|
7333
|
+
|
|
7334
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-endpoint-researcher-1", "prompt-routing-state.json");
|
|
7335
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7336
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7337
|
+
};
|
|
7338
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7339
|
+
assert.equal(state.last_triage?.destination, "researcher");
|
|
7340
|
+
assert.equal(state.last_triage?.reason, "external_reference_research");
|
|
7341
|
+
} finally {
|
|
7342
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7343
|
+
}
|
|
7344
|
+
});
|
|
7345
|
+
|
|
7346
|
+
it("routes dotted technology official-doc lookups to researcher instead of local explore", async () => {
|
|
7347
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-dotted-tech-researcher-"));
|
|
7348
|
+
try {
|
|
7349
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7350
|
+
const result = await dispatchCodexNativeHook(
|
|
7351
|
+
{
|
|
7352
|
+
hook_event_name: "UserPromptSubmit",
|
|
7353
|
+
cwd,
|
|
7354
|
+
session_id: "triage-dotted-tech-researcher-1",
|
|
7355
|
+
thread_id: "thread-triage-dotted-tech-researcher-1",
|
|
7356
|
+
turn_id: "turn-triage-dotted-tech-researcher-1",
|
|
7357
|
+
prompt: "find official docs for Node.js",
|
|
7358
|
+
},
|
|
7359
|
+
{ cwd },
|
|
7360
|
+
);
|
|
7361
|
+
|
|
7362
|
+
const additionalContext = String(
|
|
7363
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7364
|
+
);
|
|
7365
|
+
assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
|
|
7366
|
+
assert.match(additionalContext, /Prefer the researcher role surface/);
|
|
7367
|
+
|
|
7368
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-dotted-tech-researcher-1", "prompt-routing-state.json");
|
|
7369
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7370
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7371
|
+
};
|
|
7372
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7373
|
+
assert.equal(state.last_triage?.destination, "researcher");
|
|
7374
|
+
assert.equal(state.last_triage?.reason, "external_reference_research");
|
|
7375
|
+
} finally {
|
|
7376
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7377
|
+
}
|
|
7378
|
+
});
|
|
7379
|
+
|
|
7380
|
+
it("routes URL-shaped official-doc lookups with repo paths to researcher instead of local routes", async () => {
|
|
7381
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-url-path-researcher-"));
|
|
7382
|
+
try {
|
|
7383
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7384
|
+
const result = await dispatchCodexNativeHook(
|
|
7385
|
+
{
|
|
7386
|
+
hook_event_name: "UserPromptSubmit",
|
|
7387
|
+
cwd,
|
|
7388
|
+
session_id: "triage-url-path-researcher-1",
|
|
7389
|
+
thread_id: "thread-triage-url-path-researcher-1",
|
|
7390
|
+
turn_id: "turn-triage-url-path-researcher-1",
|
|
7391
|
+
prompt: "find official docs for github.com/org/repo/src/foo.ts",
|
|
7392
|
+
},
|
|
7393
|
+
{ cwd },
|
|
7394
|
+
);
|
|
7395
|
+
|
|
7396
|
+
const additionalContext = String(
|
|
7397
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7398
|
+
);
|
|
7399
|
+
assert.doesNotMatch(additionalContext, /Prefer the executor role surface/);
|
|
7400
|
+
assert.doesNotMatch(additionalContext, /Prefer the explore role surface/);
|
|
7401
|
+
assert.match(additionalContext, /Prefer the researcher role surface/);
|
|
7402
|
+
|
|
7403
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-url-path-researcher-1", "prompt-routing-state.json");
|
|
7404
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7405
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7406
|
+
};
|
|
7407
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7408
|
+
assert.equal(state.last_triage?.destination, "researcher");
|
|
7409
|
+
assert.equal(state.last_triage?.reason, "external_reference_research");
|
|
7410
|
+
} finally {
|
|
7411
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7412
|
+
}
|
|
7413
|
+
});
|
|
7414
|
+
|
|
7415
|
+
it("keeps implementation-shaped official-doc prompts on HEAVY instead of researcher", async () => {
|
|
7416
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-researcher-implementation-"));
|
|
7417
|
+
try {
|
|
7418
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7419
|
+
const result = await dispatchCodexNativeHook(
|
|
7420
|
+
{
|
|
7421
|
+
hook_event_name: "UserPromptSubmit",
|
|
7422
|
+
cwd,
|
|
7423
|
+
session_id: "triage-researcher-implementation-1",
|
|
7424
|
+
thread_id: "thread-triage-researcher-implementation-1",
|
|
7425
|
+
turn_id: "turn-triage-researcher-implementation-1",
|
|
7426
|
+
prompt: "implement auth using official docs for the SDK",
|
|
7427
|
+
},
|
|
7428
|
+
{ cwd },
|
|
7429
|
+
);
|
|
7430
|
+
|
|
7431
|
+
const additionalContext = String(
|
|
7432
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7433
|
+
);
|
|
7434
|
+
assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
|
|
7435
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
7436
|
+
|
|
7437
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-implementation-1", "prompt-routing-state.json");
|
|
7438
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7439
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7440
|
+
};
|
|
7441
|
+
assert.equal(state.last_triage?.lane, "HEAVY");
|
|
7442
|
+
assert.equal(state.last_triage?.destination, "autopilot");
|
|
7443
|
+
assert.equal(state.last_triage?.reason, "implementation_research_goal");
|
|
7444
|
+
} finally {
|
|
7445
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7446
|
+
}
|
|
7447
|
+
});
|
|
7448
|
+
|
|
7449
|
+
it("keeps planning-shaped official-doc prompts on HEAVY instead of researcher", async () => {
|
|
7450
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-researcher-planning-"));
|
|
7451
|
+
try {
|
|
7452
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7453
|
+
const result = await dispatchCodexNativeHook(
|
|
7454
|
+
{
|
|
7455
|
+
hook_event_name: "UserPromptSubmit",
|
|
7456
|
+
cwd,
|
|
7457
|
+
session_id: "triage-researcher-planning-1",
|
|
7458
|
+
thread_id: "thread-triage-researcher-planning-1",
|
|
7459
|
+
turn_id: "turn-triage-researcher-planning-1",
|
|
7460
|
+
prompt: "research and plan auth migration using official docs for the SDK",
|
|
7461
|
+
},
|
|
7462
|
+
{ cwd },
|
|
7463
|
+
);
|
|
7464
|
+
|
|
7465
|
+
const additionalContext = String(
|
|
7466
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7467
|
+
);
|
|
7468
|
+
assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
|
|
7469
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
7470
|
+
|
|
7471
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-researcher-planning-1", "prompt-routing-state.json");
|
|
7472
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7473
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7474
|
+
};
|
|
7475
|
+
assert.equal(state.last_triage?.lane, "HEAVY");
|
|
7476
|
+
assert.equal(state.last_triage?.destination, "autopilot");
|
|
7477
|
+
assert.equal(state.last_triage?.reason, "implementation_research_goal");
|
|
7478
|
+
} finally {
|
|
7479
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7480
|
+
}
|
|
7481
|
+
});
|
|
7482
|
+
|
|
7483
|
+
it("keeps local source lookup prompts off researcher", async () => {
|
|
7484
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-local-source-explore-"));
|
|
7485
|
+
try {
|
|
7486
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7487
|
+
const result = await dispatchCodexNativeHook(
|
|
7488
|
+
{
|
|
7489
|
+
hook_event_name: "UserPromptSubmit",
|
|
7490
|
+
cwd,
|
|
7491
|
+
session_id: "triage-local-source-1",
|
|
7492
|
+
thread_id: "thread-triage-local-source-1",
|
|
7493
|
+
turn_id: "turn-triage-local-source-1",
|
|
7494
|
+
prompt: "search source for parseConfig in src/config.ts",
|
|
7495
|
+
},
|
|
7496
|
+
{ cwd },
|
|
7497
|
+
);
|
|
7498
|
+
|
|
7499
|
+
const additionalContext = String(
|
|
7500
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7501
|
+
);
|
|
7502
|
+
assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
|
|
7503
|
+
assert.match(additionalContext, /Prefer the executor role surface/);
|
|
7504
|
+
|
|
7505
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-local-source-1", "prompt-routing-state.json");
|
|
7506
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7507
|
+
last_triage?: { lane?: string; destination?: string };
|
|
7508
|
+
};
|
|
7509
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7510
|
+
assert.equal(state.last_triage?.destination, "executor");
|
|
7511
|
+
} finally {
|
|
7512
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7513
|
+
}
|
|
7514
|
+
});
|
|
7515
|
+
|
|
7516
|
+
it("keeps anchored local API usage prompts on executor instead of researcher", async () => {
|
|
7517
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-local-api-executor-"));
|
|
7518
|
+
try {
|
|
7519
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7520
|
+
const result = await dispatchCodexNativeHook(
|
|
7521
|
+
{
|
|
7522
|
+
hook_event_name: "UserPromptSubmit",
|
|
7523
|
+
cwd,
|
|
7524
|
+
session_id: "triage-local-api-1",
|
|
7525
|
+
thread_id: "thread-triage-local-api-1",
|
|
7526
|
+
turn_id: "turn-triage-local-api-1",
|
|
7527
|
+
prompt: "find API usage in src/foo.ts",
|
|
7528
|
+
},
|
|
7529
|
+
{ cwd },
|
|
7530
|
+
);
|
|
7531
|
+
|
|
7532
|
+
const additionalContext = String(
|
|
7533
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7534
|
+
);
|
|
7535
|
+
assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
|
|
7536
|
+
assert.match(additionalContext, /Prefer the executor role surface/);
|
|
7537
|
+
|
|
7538
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-local-api-1", "prompt-routing-state.json");
|
|
7539
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7540
|
+
last_triage?: { lane?: string; destination?: string };
|
|
7541
|
+
};
|
|
7542
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7543
|
+
assert.equal(state.last_triage?.destination, "executor");
|
|
7544
|
+
} finally {
|
|
7545
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7546
|
+
}
|
|
7547
|
+
});
|
|
7548
|
+
|
|
7549
|
+
it("keeps project-scoped local API usage prompts on explore instead of researcher", async () => {
|
|
7550
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-project-api-explore-"));
|
|
7551
|
+
try {
|
|
7552
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7553
|
+
const result = await dispatchCodexNativeHook(
|
|
7554
|
+
{
|
|
7555
|
+
hook_event_name: "UserPromptSubmit",
|
|
7556
|
+
cwd,
|
|
7557
|
+
session_id: "triage-project-api-1",
|
|
7558
|
+
thread_id: "thread-triage-project-api-1",
|
|
7559
|
+
turn_id: "turn-triage-project-api-1",
|
|
7560
|
+
prompt: "find API usage in this project",
|
|
7561
|
+
},
|
|
7562
|
+
{ cwd },
|
|
7563
|
+
);
|
|
7564
|
+
|
|
7565
|
+
const additionalContext = String(
|
|
7566
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7567
|
+
);
|
|
7568
|
+
assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
|
|
7569
|
+
assert.match(additionalContext, /Prefer the explore role surface/);
|
|
7570
|
+
|
|
7571
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-project-api-1", "prompt-routing-state.json");
|
|
7572
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7573
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7574
|
+
};
|
|
7575
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7576
|
+
assert.equal(state.last_triage?.destination, "explore");
|
|
7577
|
+
assert.equal(state.last_triage?.reason, "local_reference_lookup");
|
|
7578
|
+
} finally {
|
|
7579
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7580
|
+
}
|
|
7581
|
+
});
|
|
7582
|
+
|
|
7583
|
+
it("keeps repository changelog lookup prompts on explore despite generic docs terms", async () => {
|
|
7584
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-repo-changelog-explore-"));
|
|
7585
|
+
try {
|
|
7586
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7587
|
+
const result = await dispatchCodexNativeHook(
|
|
7588
|
+
{
|
|
7589
|
+
hook_event_name: "UserPromptSubmit",
|
|
7590
|
+
cwd,
|
|
7591
|
+
session_id: "triage-repo-changelog-1",
|
|
7592
|
+
thread_id: "thread-triage-repo-changelog-1",
|
|
7593
|
+
turn_id: "turn-triage-repo-changelog-1",
|
|
7594
|
+
prompt: "find changelog in this repository",
|
|
7595
|
+
},
|
|
7596
|
+
{ cwd },
|
|
7597
|
+
);
|
|
7598
|
+
|
|
7599
|
+
const additionalContext = String(
|
|
7600
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7601
|
+
);
|
|
7602
|
+
assert.doesNotMatch(additionalContext, /Prefer the researcher role surface/);
|
|
7603
|
+
assert.match(additionalContext, /Prefer the explore role surface/);
|
|
7604
|
+
|
|
7605
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-repo-changelog-1", "prompt-routing-state.json");
|
|
7606
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7607
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7608
|
+
};
|
|
7609
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7610
|
+
assert.equal(state.last_triage?.destination, "explore");
|
|
7611
|
+
assert.equal(state.last_triage?.reason, "local_reference_lookup");
|
|
7612
|
+
} finally {
|
|
7613
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7614
|
+
}
|
|
7615
|
+
});
|
|
7616
|
+
|
|
7617
|
+
it("routes anchored read-only questions through explore before executor", async () => {
|
|
7618
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-anchored-question-explore-"));
|
|
7619
|
+
try {
|
|
7620
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
7621
|
+
const result = await dispatchCodexNativeHook(
|
|
7622
|
+
{
|
|
7623
|
+
hook_event_name: "UserPromptSubmit",
|
|
7624
|
+
cwd,
|
|
7625
|
+
session_id: "triage-anchored-question-1",
|
|
7626
|
+
thread_id: "thread-triage-anchored-question-1",
|
|
7627
|
+
turn_id: "turn-triage-anchored-question-1",
|
|
7628
|
+
prompt: "what does src/foo.ts do?",
|
|
7629
|
+
},
|
|
7630
|
+
{ cwd },
|
|
7631
|
+
);
|
|
7632
|
+
|
|
7633
|
+
const additionalContext = String(
|
|
7634
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
7635
|
+
);
|
|
7636
|
+
assert.doesNotMatch(additionalContext, /Prefer the executor role surface/);
|
|
7637
|
+
assert.match(additionalContext, /Prefer the explore role surface/);
|
|
7638
|
+
|
|
7639
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-anchored-question-1", "prompt-routing-state.json");
|
|
7640
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
7641
|
+
last_triage?: { lane?: string; destination?: string; reason?: string };
|
|
7642
|
+
};
|
|
7643
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
7644
|
+
assert.equal(state.last_triage?.destination, "explore");
|
|
7645
|
+
assert.equal(state.last_triage?.reason, "question_or_explanation");
|
|
7646
|
+
} finally {
|
|
7647
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7648
|
+
}
|
|
7649
|
+
});
|
|
7650
|
+
|
|
5569
7651
|
// ── Group 6: PASS (no triage injection, no state) ────────────────────────
|
|
5570
7652
|
|
|
5571
7653
|
it("produces no triage advisory and no state for trivial greeting prompts", async () => {
|