oh-my-codex 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +10 -7
- package/Cargo.toml +1 -1
- package/README.md +3 -0
- package/crates/omx-explore/Cargo.toml +3 -0
- package/crates/omx-explore/src/main.rs +517 -16
- package/dist/agents/__tests__/native-config.test.js +33 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/autoresearch/goal.d.ts +90 -0
- package/dist/autoresearch/goal.d.ts.map +1 -0
- package/dist/autoresearch/goal.js +237 -0
- package/dist/autoresearch/goal.js.map +1 -0
- package/dist/autoresearch/skill-validation.d.ts +1 -0
- package/dist/autoresearch/skill-validation.d.ts.map +1 -1
- package/dist/autoresearch/skill-validation.js +10 -3
- package/dist/autoresearch/skill-validation.js.map +1 -1
- package/dist/catalog/__tests__/generator.test.js +9 -4
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +29 -2
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
- package/dist/catalog/__tests__/schema.test.js +14 -3
- package/dist/catalog/__tests__/schema.test.js.map +1 -1
- package/dist/catalog/schema.js +1 -1
- package/dist/catalog/schema.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.d.ts +2 -0
- package/dist/cli/__tests__/autoresearch-goal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/autoresearch-goal.test.js +194 -0
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -0
- package/dist/cli/__tests__/cleanup.test.js +82 -1
- package/dist/cli/__tests__/cleanup.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +7 -4
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +122 -0
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +25 -2
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +1 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +48 -18
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +222 -10
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +58 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-serve.test.js +27 -1
- package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +26 -1
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +2 -2
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/performance-goal.test.d.ts +2 -0
- package/dist/cli/__tests__/performance-goal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/performance-goal.test.js +144 -0
- package/dist/cli/__tests__/performance-goal.test.js.map +1 -0
- package/dist/cli/__tests__/question.test.js +8 -0
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts +2 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +31 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -0
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +5 -4
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js +7 -0
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +59 -1
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +57 -21
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +27 -8
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +20 -10
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skill-validation.test.js +11 -11
- package/dist/cli/__tests__/setup-skill-validation.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +12 -12
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +242 -10
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.d.ts +2 -0
- package/dist/cli/__tests__/ultragoal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ultragoal.test.js +106 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -0
- package/dist/cli/__tests__/uninstall.test.js +11 -0
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/autoresearch-goal.d.ts +3 -0
- package/dist/cli/autoresearch-goal.d.ts.map +1 -0
- package/dist/cli/autoresearch-goal.js +175 -0
- package/dist/cli/autoresearch-goal.js.map +1 -0
- package/dist/cli/cleanup.d.ts +3 -1
- package/dist/cli/cleanup.d.ts.map +1 -1
- package/dist/cli/cleanup.js +42 -2
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +95 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +10 -2
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +21 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +268 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-serve.d.ts +1 -0
- package/dist/cli/mcp-serve.d.ts.map +1 -1
- package/dist/cli/mcp-serve.js +8 -0
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/native-assets.js +1 -1
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/performance-goal.d.ts +3 -0
- package/dist/cli/performance-goal.d.ts.map +1 -0
- package/dist/cli/performance-goal.js +186 -0
- package/dist/cli/performance-goal.js.map +1 -0
- package/dist/cli/ralph.d.ts +2 -0
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +25 -1
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +13 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts +6 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +113 -33
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/tmux-hook.d.ts.map +1 -1
- package/dist/cli/tmux-hook.js +2 -1
- package/dist/cli/tmux-hook.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +3 -0
- package/dist/cli/ultragoal.d.ts.map +1 -0
- package/dist/cli/ultragoal.js +191 -0
- package/dist/cli/ultragoal.js.map +1 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +4 -2
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +39 -6
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -0
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/commit-lore-guard.d.ts +3 -0
- package/dist/config/commit-lore-guard.d.ts.map +1 -0
- package/dist/config/commit-lore-guard.js +9 -0
- package/dist/config/commit-lore-guard.js.map +1 -0
- package/dist/config/generator.d.ts +14 -4
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +166 -66
- package/dist/config/generator.js.map +1 -1
- package/dist/config/omx-first-party-mcp.d.ts +1 -0
- package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
- package/dist/config/omx-first-party-mcp.js +4 -1
- package/dist/config/omx-first-party-mcp.js.map +1 -1
- package/dist/goal-workflows/__tests__/artifacts.test.d.ts +2 -0
- package/dist/goal-workflows/__tests__/artifacts.test.d.ts.map +1 -0
- package/dist/goal-workflows/__tests__/artifacts.test.js +96 -0
- package/dist/goal-workflows/__tests__/artifacts.test.js.map +1 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts +2 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts.map +1 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +54 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -0
- package/dist/goal-workflows/artifacts.d.ts +62 -0
- package/dist/goal-workflows/artifacts.d.ts.map +1 -0
- package/dist/goal-workflows/artifacts.js +132 -0
- package/dist/goal-workflows/artifacts.js.map +1 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +28 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -0
- package/dist/goal-workflows/codex-goal-snapshot.js +110 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -0
- package/dist/goal-workflows/handoff.d.ts +10 -0
- package/dist/goal-workflows/handoff.d.ts.map +1 -0
- package/dist/goal-workflows/handoff.js +31 -0
- package/dist/goal-workflows/handoff.js.map +1 -0
- package/dist/goal-workflows/validation.d.ts +13 -0
- package/dist/goal-workflows/validation.d.ts.map +1 -0
- package/dist/goal-workflows/validation.js +36 -0
- package/dist/goal-workflows/validation.js.map +1 -0
- package/dist/hooks/__tests__/agents-overlay.test.js +59 -0
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +109 -18
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +45 -32
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +3 -3
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +17 -24
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +3 -3
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/__tests__/task-size-detector.test.js +1 -1
- package/dist/hooks/__tests__/task-size-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js +3 -3
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js +7 -11
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +23 -2
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +12 -13
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +2 -10
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +0 -4
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/session.js +2 -2
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/task-size-detector.d.ts.map +1 -1
- package/dist/hooks/task-size-detector.js +1 -0
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +30 -14
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +29 -7
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/reconcile.d.ts +2 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +12 -0
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +15 -2
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +54 -0
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +36 -0
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +1 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +9 -7
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +17 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +36 -2
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +26 -0
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts +1 -0
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +35 -5
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/http-client.test.d.ts +2 -0
- package/dist/notifications/__tests__/http-client.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/http-client.test.js +90 -0
- package/dist/notifications/__tests__/http-client.test.js.map +1 -0
- package/dist/notifications/__tests__/notifier.test.js +22 -60
- package/dist/notifications/__tests__/notifier.test.js.map +1 -1
- package/dist/notifications/dispatcher.d.ts.map +1 -1
- package/dist/notifications/dispatcher.js +35 -60
- package/dist/notifications/dispatcher.js.map +1 -1
- package/dist/notifications/http-client.d.ts +22 -0
- package/dist/notifications/http-client.d.ts.map +1 -0
- package/dist/notifications/http-client.js +298 -0
- package/dist/notifications/http-client.js.map +1 -0
- package/dist/notifications/notifier.d.ts +3 -2
- package/dist/notifications/notifier.d.ts.map +1 -1
- package/dist/notifications/notifier.js +17 -22
- package/dist/notifications/notifier.js.map +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +63 -2
- package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
- package/dist/openclaw/dispatcher.d.ts.map +1 -1
- package/dist/openclaw/dispatcher.js +3 -2
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/performance-goal/artifacts.d.ts +76 -0
- package/dist/performance-goal/artifacts.d.ts.map +1 -0
- package/dist/performance-goal/artifacts.js +221 -0
- package/dist/performance-goal/artifacts.js.map +1 -0
- package/dist/pipeline/__tests__/stages.test.js +423 -14
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/team-exec.d.ts +8 -4
- package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
- package/dist/pipeline/stages/team-exec.js +181 -13
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/planning/__tests__/artifacts.test.js +261 -1
- package/dist/planning/__tests__/artifacts.test.js.map +1 -1
- package/dist/planning/artifact-names.d.ts +13 -0
- package/dist/planning/artifact-names.d.ts.map +1 -0
- package/dist/planning/artifact-names.js +108 -0
- package/dist/planning/artifact-names.js.map +1 -0
- package/dist/planning/artifacts.d.ts +23 -1
- package/dist/planning/artifacts.d.ts.map +1 -1
- package/dist/planning/artifacts.js +171 -59
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/ralph/__tests__/persistence.test.js +21 -1
- package/dist/ralph/__tests__/persistence.test.js.map +1 -1
- package/dist/ralph/persistence.d.ts.map +1 -1
- package/dist/ralph/persistence.js +6 -4
- package/dist/ralph/persistence.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +2 -0
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +6 -0
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1749 -88
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js +33 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/run-test-files.test.js +36 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +570 -45
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +7 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +341 -15
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/hook-derived-watcher.js +2 -1
- package/dist/scripts/hook-derived-watcher.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +2 -1
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +1 -2
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.js +2 -3
- package/dist/scripts/notify-hook/orchestration-intent.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +0 -2
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +8 -60
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-posttooluse.js +1 -1
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts +15 -0
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -0
- package/dist/scripts/notify-hook/team-worker-stop.js +224 -0
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +26 -18
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook.js +1 -1
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/run-test-files.js +17 -1
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts +1 -0
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +10 -4
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +26 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +76 -0
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/operations.d.ts +3 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +8 -4
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +1 -0
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +54 -13
- package/dist/state/skill-active.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +279 -0
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.d.ts +2 -0
- package/dist/team/__tests__/approved-execution.test.d.ts.map +1 -0
- package/dist/team/__tests__/approved-execution.test.js +124 -0
- package/dist/team/__tests__/approved-execution.test.js.map +1 -0
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +2 -4
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/delivery-log.test.d.ts +2 -0
- package/dist/team/__tests__/delivery-log.test.d.ts.map +1 -0
- package/dist/team/__tests__/delivery-log.test.js +44 -0
- package/dist/team/__tests__/delivery-log.test.js.map +1 -0
- package/dist/team/__tests__/model-contract.test.js +40 -9
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/repo-aware-decomposition.test.js +41 -0
- package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js +4 -4
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime-boxed-state.test.d.ts +2 -0
- package/dist/team/__tests__/runtime-boxed-state.test.d.ts.map +1 -0
- package/dist/team/__tests__/runtime-boxed-state.test.js +39 -0
- package/dist/team/__tests__/runtime-boxed-state.test.js.map +1 -0
- package/dist/team/__tests__/runtime-cli.test.js +24 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +563 -72
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +13 -0
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +13 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-identity.test.d.ts +2 -0
- package/dist/team/__tests__/team-identity.test.d.ts.map +1 -0
- package/dist/team/__tests__/team-identity.test.js +166 -0
- package/dist/team/__tests__/team-identity.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.test.js +58 -1
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +62 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/api-interop.d.ts +1 -0
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +163 -132
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/approved-execution.d.ts +37 -0
- package/dist/team/approved-execution.d.ts.map +1 -0
- package/dist/team/approved-execution.js +136 -0
- package/dist/team/approved-execution.js.map +1 -0
- package/dist/team/delivery-log.d.ts +1 -1
- package/dist/team/delivery-log.d.ts.map +1 -1
- package/dist/team/delivery-log.js +2 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/followup-planner.js +2 -2
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/goal-workflow.d.ts +20 -0
- package/dist/team/goal-workflow.d.ts.map +1 -0
- package/dist/team/goal-workflow.js +57 -0
- package/dist/team/goal-workflow.js.map +1 -0
- package/dist/team/orchestrator.js +2 -2
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/repo-aware-decomposition.d.ts +3 -0
- package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
- package/dist/team/repo-aware-decomposition.js +2 -0
- package/dist/team/repo-aware-decomposition.js.map +1 -1
- package/dist/team/role-router.js +5 -5
- package/dist/team/role-router.js.map +1 -1
- package/dist/team/runtime-cli.d.ts +32 -2
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +78 -26
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +7 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +383 -40
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +2 -0
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state.d.ts +9 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +21 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts +26 -0
- package/dist/team/team-identity.d.ts.map +1 -0
- package/dist/team/team-identity.js +169 -0
- package/dist/team/team-identity.js.map +1 -0
- package/dist/team/tmux-session.d.ts +18 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +65 -3
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +4 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +28 -2
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.d.ts +2 -0
- package/dist/ultragoal/__tests__/artifacts.test.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/artifacts.test.js +93 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -0
- package/dist/ultragoal/artifacts.d.ts +89 -0
- package/dist/ultragoal/artifacts.d.ts.map +1 -0
- package/dist/ultragoal/artifacts.js +233 -0
- package/dist/ultragoal/artifacts.js.map +1 -0
- package/dist/utils/__tests__/agents-model-table.test.js +3 -1
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +31 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +12 -1
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +23 -7
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +30 -19
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +5 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +30 -5
- package/plugins/oh-my-codex/skills/ask/SKILL.md +58 -0
- package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +36 -0
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +65 -0
- package/plugins/oh-my-codex/skills/plan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -3
- package/plugins/oh-my-codex/skills/team/SKILL.md +6 -2
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +49 -0
- package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +9 -9
- package/prompts/api-reviewer.md +1 -1
- package/prompts/code-reviewer.md +2 -0
- package/prompts/performance-reviewer.md +1 -1
- package/prompts/quality-reviewer.md +1 -1
- package/prompts/quality-strategist.md +2 -2
- package/prompts/style-reviewer.md +1 -1
- package/prompts/test-engineer.md +1 -1
- package/skills/ai-slop-cleaner/SKILL.md +30 -5
- package/skills/ask/SKILL.md +58 -0
- package/skills/ask-claude/SKILL.md +3 -54
- package/skills/ask-gemini/SKILL.md +3 -54
- package/skills/autoresearch-goal/SKILL.md +36 -0
- package/skills/build-fix/SKILL.md +4 -139
- package/skills/deepsearch/SKILL.md +4 -32
- package/skills/ecomode/SKILL.md +4 -108
- package/skills/help/SKILL.md +4 -196
- package/skills/note/SKILL.md +4 -56
- package/skills/omx-setup/SKILL.md +2 -2
- package/skills/performance-goal/SKILL.md +65 -0
- package/skills/plan/SKILL.md +1 -1
- package/skills/ralph/SKILL.md +22 -3
- package/skills/ralph-init/SKILL.md +4 -40
- package/skills/review/SKILL.md +4 -32
- package/skills/security-review/SKILL.md +4 -294
- package/skills/swarm/SKILL.md +4 -19
- package/skills/tdd/SKILL.md +4 -100
- package/skills/team/SKILL.md +6 -2
- package/skills/trace/SKILL.md +4 -27
- package/skills/ultragoal/SKILL.md +49 -0
- package/skills/visual-ralph/SKILL.md +9 -9
- package/skills/visual-verdict/SKILL.md +4 -70
- package/skills/web-clone/SKILL.md +4 -18
- package/src/scripts/__tests__/codex-native-hook.test.ts +2923 -1030
- package/src/scripts/__tests__/hook-derived-watcher.test.ts +45 -1
- package/src/scripts/__tests__/run-test-files.test.ts +46 -0
- package/src/scripts/codex-native-hook.ts +696 -46
- package/src/scripts/codex-native-pre-post.ts +369 -16
- package/src/scripts/hook-derived-watcher.ts +2 -1
- package/src/scripts/notify-fallback-watcher.ts +2 -1
- package/src/scripts/notify-hook/orchestration-intent.ts +1 -3
- package/src/scripts/notify-hook/team-leader-nudge.ts +7 -63
- package/src/scripts/notify-hook/team-worker-posttooluse.ts +1 -1
- package/src/scripts/notify-hook/team-worker-stop.ts +246 -0
- package/src/scripts/notify-hook/team-worker.ts +23 -14
- package/src/scripts/notify-hook.ts +1 -1
- package/src/scripts/run-test-files.ts +20 -1
- package/src/scripts/sync-plugin-mirror.ts +13 -4
- package/templates/catalog-manifest.json +45 -27
- package/plugins/oh-my-codex/skills/ask-claude/SKILL.md +0 -61
- package/plugins/oh-my-codex/skills/ask-gemini/SKILL.md +0 -61
- package/plugins/oh-my-codex/skills/help/SKILL.md +0 -202
- package/plugins/oh-my-codex/skills/note/SKILL.md +0 -62
- package/plugins/oh-my-codex/skills/security-review/SKILL.md +0 -300
- package/plugins/oh-my-codex/skills/trace/SKILL.md +0 -33
- package/plugins/oh-my-codex/skills/visual-verdict/SKILL.md +0 -76
|
@@ -13,6 +13,7 @@ import { dispatchCodexNativeHook, isCodexNativeHookMainModule, mapCodexHookEvent
|
|
|
13
13
|
import { writeSessionStart } from "../../hooks/session.js";
|
|
14
14
|
import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
15
15
|
import { executeStateOperation } from "../../state/operations.js";
|
|
16
|
+
import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
|
|
16
17
|
function nativeHookScriptPath() {
|
|
17
18
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
18
19
|
}
|
|
@@ -35,6 +36,50 @@ async function writeJson(path, value) {
|
|
|
35
36
|
await mkdir(dirname(path), { recursive: true }).catch(() => { });
|
|
36
37
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
37
38
|
}
|
|
39
|
+
function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
|
|
40
|
+
return `#!/usr/bin/env bash
|
|
41
|
+
set -eu
|
|
42
|
+
echo "$@" >> "${tmuxLogPath}"
|
|
43
|
+
cmd="$1"
|
|
44
|
+
shift || true
|
|
45
|
+
if [[ "$cmd" == "display-message" ]]; then
|
|
46
|
+
fmt=""
|
|
47
|
+
while [[ "$#" -gt 0 ]]; do
|
|
48
|
+
case "$1" in
|
|
49
|
+
-p) ;;
|
|
50
|
+
-t) shift ;;
|
|
51
|
+
*) fmt="$1" ;;
|
|
52
|
+
esac
|
|
53
|
+
shift || true
|
|
54
|
+
done
|
|
55
|
+
case "$fmt" in
|
|
56
|
+
"#{pane_in_mode}") echo "0" ;;
|
|
57
|
+
"#{pane_id}") echo "%42" ;;
|
|
58
|
+
"#{pane_current_path}") pwd ;;
|
|
59
|
+
"#{pane_start_command}") echo "codex" ;;
|
|
60
|
+
"#{pane_current_command}") echo "codex" ;;
|
|
61
|
+
"#S") echo "omx-team-worker-stop" ;;
|
|
62
|
+
*) ;;
|
|
63
|
+
esac
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
if [[ "$cmd" == "capture-pane" ]]; then
|
|
67
|
+
echo "› ready"
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
if [[ "$cmd" == "send-keys" ]]; then
|
|
71
|
+
${options.failSend ? "exit 1" : "exit 0"}
|
|
72
|
+
fi
|
|
73
|
+
exit 0
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
async function initTempGitRepo(prefix) {
|
|
77
|
+
const cwd = await mkdtemp(join(tmpdir(), prefix));
|
|
78
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
79
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
80
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
81
|
+
return cwd;
|
|
82
|
+
}
|
|
38
83
|
async function writeActiveAutopilotSession(cwd, sessionId) {
|
|
39
84
|
await writeJson(join(cwd, ".omx", "state", "session.json"), {
|
|
40
85
|
session_id: sessionId,
|
|
@@ -96,10 +141,12 @@ const TEAM_ENV_KEYS = [
|
|
|
96
141
|
"OMX_TEAM_STATE_ROOT",
|
|
97
142
|
"OMX_TEAM_LEADER_CWD",
|
|
98
143
|
"OMX_SESSION_ID",
|
|
144
|
+
"SESSION_ID",
|
|
99
145
|
"OMX_QUESTION_RETURN_PANE",
|
|
100
146
|
"OMX_LEADER_PANE_ID",
|
|
101
147
|
"TMUX",
|
|
102
148
|
"TMUX_PANE",
|
|
149
|
+
"OMX_TMUX_HUD_OWNER",
|
|
103
150
|
];
|
|
104
151
|
const priorTeamEnv = new Map();
|
|
105
152
|
beforeEach(() => {
|
|
@@ -763,6 +810,114 @@ describe("codex native hook dispatch", () => {
|
|
|
763
810
|
await rm(cwd, { recursive: true, force: true });
|
|
764
811
|
}
|
|
765
812
|
});
|
|
813
|
+
it("warns completion-like prompts when active goal workflows need Codex snapshot reconciliation", async () => {
|
|
814
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-warning-"));
|
|
815
|
+
try {
|
|
816
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
817
|
+
version: 1,
|
|
818
|
+
activeGoalId: "G001-demo",
|
|
819
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
820
|
+
});
|
|
821
|
+
const result = await dispatchCodexNativeHook({
|
|
822
|
+
hook_event_name: "UserPromptSubmit",
|
|
823
|
+
cwd,
|
|
824
|
+
session_id: "sess-goal-warning",
|
|
825
|
+
thread_id: "thread-goal-warning",
|
|
826
|
+
prompt: "complete this goal now",
|
|
827
|
+
}, { cwd });
|
|
828
|
+
assert.match(JSON.stringify(result.outputJson), /requires Codex goal snapshot reconciliation/);
|
|
829
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal/);
|
|
830
|
+
assert.match(JSON.stringify(result.outputJson), /--codex-goal-json/);
|
|
831
|
+
}
|
|
832
|
+
finally {
|
|
833
|
+
await rm(cwd, { recursive: true, force: true });
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
it("blocks Stop when a completion-like final answer skips active goal snapshot reconciliation", async () => {
|
|
837
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-stop-"));
|
|
838
|
+
try {
|
|
839
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
840
|
+
version: 1,
|
|
841
|
+
workflow: "performance-goal",
|
|
842
|
+
slug: "latency",
|
|
843
|
+
objective: "Reduce latency",
|
|
844
|
+
status: "validation_passed",
|
|
845
|
+
});
|
|
846
|
+
const result = await dispatchCodexNativeHook({
|
|
847
|
+
hook_event_name: "Stop",
|
|
848
|
+
cwd,
|
|
849
|
+
session_id: "sess-goal-stop",
|
|
850
|
+
thread_id: "thread-goal-stop",
|
|
851
|
+
last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
|
|
852
|
+
}, { cwd });
|
|
853
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
854
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
855
|
+
assert.match(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
|
|
856
|
+
assert.match(JSON.stringify(result.outputJson), /Hooks must not mutate Codex goal state/);
|
|
857
|
+
}
|
|
858
|
+
finally {
|
|
859
|
+
await rm(cwd, { recursive: true, force: true });
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
|
|
863
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
|
|
864
|
+
try {
|
|
865
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
866
|
+
const canonicalSessionId = "sess-parent";
|
|
867
|
+
const leaderNativeSessionId = "native-parent-thread";
|
|
868
|
+
const childNativeSessionId = "native-child-thread";
|
|
869
|
+
const nowIso = new Date().toISOString();
|
|
870
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
871
|
+
session_id: canonicalSessionId,
|
|
872
|
+
native_session_id: leaderNativeSessionId,
|
|
873
|
+
});
|
|
874
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
875
|
+
schemaVersion: 1,
|
|
876
|
+
sessions: {
|
|
877
|
+
[canonicalSessionId]: {
|
|
878
|
+
session_id: canonicalSessionId,
|
|
879
|
+
leader_thread_id: leaderNativeSessionId,
|
|
880
|
+
updated_at: nowIso,
|
|
881
|
+
threads: {
|
|
882
|
+
[leaderNativeSessionId]: {
|
|
883
|
+
thread_id: leaderNativeSessionId,
|
|
884
|
+
kind: "leader",
|
|
885
|
+
first_seen_at: nowIso,
|
|
886
|
+
last_seen_at: nowIso,
|
|
887
|
+
turn_count: 1,
|
|
888
|
+
},
|
|
889
|
+
[childNativeSessionId]: {
|
|
890
|
+
thread_id: childNativeSessionId,
|
|
891
|
+
kind: "subagent",
|
|
892
|
+
first_seen_at: nowIso,
|
|
893
|
+
last_seen_at: nowIso,
|
|
894
|
+
turn_count: 1,
|
|
895
|
+
mode: "architect",
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
const result = await dispatchCodexNativeHook({
|
|
902
|
+
hook_event_name: "UserPromptSubmit",
|
|
903
|
+
cwd,
|
|
904
|
+
session_id: childNativeSessionId,
|
|
905
|
+
thread_id: childNativeSessionId,
|
|
906
|
+
turn_id: "turn-child-1",
|
|
907
|
+
prompt: "$ralplan Architect review step. Review the draft plan and return APPROVE or ITERATE.",
|
|
908
|
+
}, { cwd });
|
|
909
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
910
|
+
assert.equal(result.skillState, null);
|
|
911
|
+
assert.equal(result.outputJson, null);
|
|
912
|
+
assert.equal(existsSync(join(stateDir, "skill-active-state.json")), false);
|
|
913
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), false);
|
|
914
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
|
|
915
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
|
|
916
|
+
}
|
|
917
|
+
finally {
|
|
918
|
+
await rm(cwd, { recursive: true, force: true });
|
|
919
|
+
}
|
|
920
|
+
});
|
|
766
921
|
it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
|
|
767
922
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
|
|
768
923
|
try {
|
|
@@ -786,6 +941,33 @@ describe("codex native hook dispatch", () => {
|
|
|
786
941
|
await rm(cwd, { recursive: true, force: true });
|
|
787
942
|
}
|
|
788
943
|
});
|
|
944
|
+
it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
|
|
945
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
|
|
946
|
+
try {
|
|
947
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
948
|
+
const result = await dispatchCodexNativeHook({
|
|
949
|
+
hook_event_name: "UserPromptSubmit",
|
|
950
|
+
cwd,
|
|
951
|
+
session_id: "sess-ultragoal-1",
|
|
952
|
+
thread_id: "thread-ultragoal-1",
|
|
953
|
+
turn_id: "turn-ultragoal-1",
|
|
954
|
+
prompt: "$ultragoal split this launch into durable goals",
|
|
955
|
+
}, { cwd });
|
|
956
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
957
|
+
assert.equal(result.skillState?.skill, "ultragoal");
|
|
958
|
+
assert.equal(result.skillState?.initialized_mode, undefined);
|
|
959
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
960
|
+
assert.match(message, /"\$ultragoal" -> ultragoal/);
|
|
961
|
+
assert.match(message, /Ultragoal protocol:/);
|
|
962
|
+
assert.match(message, /get_goal/);
|
|
963
|
+
assert.match(message, /create_goal/);
|
|
964
|
+
assert.match(message, /update_goal/);
|
|
965
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
|
|
966
|
+
}
|
|
967
|
+
finally {
|
|
968
|
+
await rm(cwd, { recursive: true, force: true });
|
|
969
|
+
}
|
|
970
|
+
});
|
|
789
971
|
it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
|
|
790
972
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
|
|
791
973
|
try {
|
|
@@ -1505,10 +1687,12 @@ export async function onHookEvent(event) {
|
|
|
1505
1687
|
const originalTmux = process.env.TMUX;
|
|
1506
1688
|
const originalTmuxPane = process.env.TMUX_PANE;
|
|
1507
1689
|
const originalPath = process.env.PATH;
|
|
1690
|
+
const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
1508
1691
|
const originalArgv = process.argv;
|
|
1509
1692
|
try {
|
|
1510
1693
|
process.env.TMUX = "1";
|
|
1511
1694
|
process.env.TMUX_PANE = "%1";
|
|
1695
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
|
|
1512
1696
|
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1513
1697
|
await writeFile(join(cwd, ".omx", "hud-config.json"), JSON.stringify({ preset: "focused", git: { display: "branch" } }, null, 2));
|
|
1514
1698
|
const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-bin-"));
|
|
@@ -1560,11 +1744,61 @@ esac
|
|
|
1560
1744
|
else {
|
|
1561
1745
|
process.env.TMUX_PANE = originalTmuxPane;
|
|
1562
1746
|
}
|
|
1747
|
+
if (originalHudOwner === undefined) {
|
|
1748
|
+
delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
|
|
1752
|
+
}
|
|
1563
1753
|
process.env.PATH = originalPath;
|
|
1564
1754
|
process.argv = originalArgv;
|
|
1565
1755
|
await rm(cwd, { recursive: true, force: true });
|
|
1566
1756
|
}
|
|
1567
1757
|
});
|
|
1758
|
+
it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
|
|
1759
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
|
|
1760
|
+
const originalTmux = process.env.TMUX;
|
|
1761
|
+
const originalTmuxPane = process.env.TMUX_PANE;
|
|
1762
|
+
const originalPath = process.env.PATH;
|
|
1763
|
+
const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
1764
|
+
try {
|
|
1765
|
+
process.env.TMUX = "1";
|
|
1766
|
+
process.env.TMUX_PANE = "%claude";
|
|
1767
|
+
delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
1768
|
+
const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-bin-"));
|
|
1769
|
+
const tmuxLog = join(cwd, "tmux.log");
|
|
1770
|
+
await writeFile(join(binDir, "tmux"), `#!/usr/bin/env bash
|
|
1771
|
+
printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
|
|
1772
|
+
exit 0
|
|
1773
|
+
`);
|
|
1774
|
+
await chmod(join(binDir, "tmux"), 0o755);
|
|
1775
|
+
process.env.PATH = `${binDir}:${originalPath}`;
|
|
1776
|
+
const result = await dispatchCodexNativeHook({
|
|
1777
|
+
hook_event_name: "UserPromptSubmit",
|
|
1778
|
+
cwd,
|
|
1779
|
+
session_id: "sess-hud-unowned",
|
|
1780
|
+
prompt: "$ralplan prepare plan",
|
|
1781
|
+
}, { cwd });
|
|
1782
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1783
|
+
assert.equal(existsSync(tmuxLog), false);
|
|
1784
|
+
}
|
|
1785
|
+
finally {
|
|
1786
|
+
if (originalTmux === undefined)
|
|
1787
|
+
delete process.env.TMUX;
|
|
1788
|
+
else
|
|
1789
|
+
process.env.TMUX = originalTmux;
|
|
1790
|
+
if (originalTmuxPane === undefined)
|
|
1791
|
+
delete process.env.TMUX_PANE;
|
|
1792
|
+
else
|
|
1793
|
+
process.env.TMUX_PANE = originalTmuxPane;
|
|
1794
|
+
if (originalHudOwner === undefined)
|
|
1795
|
+
delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
1796
|
+
else
|
|
1797
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
|
|
1798
|
+
process.env.PATH = originalPath;
|
|
1799
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1568
1802
|
it("blocks Bash omx question when no leader-pane return hint is preserved", async () => {
|
|
1569
1803
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-enforce-"));
|
|
1570
1804
|
try {
|
|
@@ -1933,48 +2167,45 @@ esac
|
|
|
1933
2167
|
await rm(cwd, { recursive: true, force: true });
|
|
1934
2168
|
}
|
|
1935
2169
|
});
|
|
1936
|
-
it("
|
|
1937
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2170
|
+
it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
|
|
2171
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
|
|
1938
2172
|
try {
|
|
1939
2173
|
const result = await dispatchCodexNativeHook({
|
|
1940
2174
|
hook_event_name: "PreToolUse",
|
|
1941
2175
|
cwd,
|
|
1942
2176
|
tool_name: "Bash",
|
|
1943
|
-
tool_use_id: "tool-
|
|
1944
|
-
tool_input: {
|
|
2177
|
+
tool_use_id: "tool-slop-warn",
|
|
2178
|
+
tool_input: {
|
|
2179
|
+
command: [
|
|
2180
|
+
"cat > src/runtime.ts <<'EOF'",
|
|
2181
|
+
"export function loadRuntime() {",
|
|
2182
|
+
" // implement a quick hack fallback if it fails",
|
|
2183
|
+
" return process.env.RUNTIME || 'local';",
|
|
2184
|
+
"}",
|
|
2185
|
+
"EOF",
|
|
2186
|
+
].join("\n"),
|
|
2187
|
+
},
|
|
1945
2188
|
}, { cwd });
|
|
1946
2189
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1947
|
-
assert.
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
},
|
|
1953
|
-
systemMessage: [
|
|
1954
|
-
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1955
|
-
"- Add a blank line after the subject before the narrative body.",
|
|
1956
|
-
"- Add a narrative body paragraph explaining the decision context.",
|
|
1957
|
-
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
1958
|
-
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
1959
|
-
].join("\n"),
|
|
1960
|
-
});
|
|
1961
|
-
const hookSpecificOutput = result.outputJson
|
|
1962
|
-
.hookSpecificOutput ?? {};
|
|
1963
|
-
assert.equal("additionalContext" in hookSpecificOutput, false);
|
|
2190
|
+
assert.equal(result.outputJson?.decision, undefined);
|
|
2191
|
+
assert.equal(result.outputJson?.hookSpecificOutput?.hookEventName, "PreToolUse");
|
|
2192
|
+
assert.match(JSON.stringify(result.outputJson), /don't make potential slop/);
|
|
2193
|
+
assert.match(JSON.stringify(result.outputJson), /architect/);
|
|
2194
|
+
assert.match(JSON.stringify(result.outputJson), /environment issue/);
|
|
1964
2195
|
}
|
|
1965
2196
|
finally {
|
|
1966
2197
|
await rm(cwd, { recursive: true, force: true });
|
|
1967
2198
|
}
|
|
1968
2199
|
});
|
|
1969
|
-
it("
|
|
1970
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2200
|
+
it("does not warn on PreToolUse for read-only fallback text inspection", async () => {
|
|
2201
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-readonly-"));
|
|
1971
2202
|
try {
|
|
1972
2203
|
const result = await dispatchCodexNativeHook({
|
|
1973
2204
|
hook_event_name: "PreToolUse",
|
|
1974
2205
|
cwd,
|
|
1975
2206
|
tool_name: "Bash",
|
|
1976
|
-
tool_use_id: "tool-
|
|
1977
|
-
tool_input: { command: "
|
|
2207
|
+
tool_use_id: "tool-slop-readonly",
|
|
2208
|
+
tool_input: { command: "rg \"quick hack fallback if it fails\" src docs" },
|
|
1978
2209
|
}, { cwd });
|
|
1979
2210
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1980
2211
|
assert.equal(result.outputJson, null);
|
|
@@ -1983,32 +2214,53 @@ esac
|
|
|
1983
2214
|
await rm(cwd, { recursive: true, force: true });
|
|
1984
2215
|
}
|
|
1985
2216
|
});
|
|
1986
|
-
it("
|
|
1987
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2217
|
+
it("warns when a read-only command is chained before sloppy fallback writes", async () => {
|
|
2218
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-chained-write-"));
|
|
1988
2219
|
try {
|
|
1989
2220
|
const result = await dispatchCodexNativeHook({
|
|
1990
2221
|
hook_event_name: "PreToolUse",
|
|
1991
2222
|
cwd,
|
|
1992
2223
|
tool_name: "Bash",
|
|
1993
|
-
tool_use_id: "tool-
|
|
1994
|
-
tool_input: {
|
|
2224
|
+
tool_use_id: "tool-slop-chained-write",
|
|
2225
|
+
tool_input: {
|
|
2226
|
+
command: [
|
|
2227
|
+
"rg foo src && cat > src/runtime.ts <<EOF",
|
|
2228
|
+
"export function loadRuntime() {",
|
|
2229
|
+
" // implement quick hack fallback if it fails",
|
|
2230
|
+
" return 'local';",
|
|
2231
|
+
"}",
|
|
2232
|
+
"EOF",
|
|
2233
|
+
].join("\n"),
|
|
2234
|
+
},
|
|
1995
2235
|
}, { cwd });
|
|
1996
2236
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
1997
|
-
assert.equal(result.outputJson,
|
|
2237
|
+
assert.equal(result.outputJson?.decision, undefined);
|
|
2238
|
+
assert.equal(result.outputJson?.hookSpecificOutput?.hookEventName, "PreToolUse");
|
|
2239
|
+
assert.match(JSON.stringify(result.outputJson), /don't make potential slop/);
|
|
1998
2240
|
}
|
|
1999
2241
|
finally {
|
|
2000
2242
|
await rm(cwd, { recursive: true, force: true });
|
|
2001
2243
|
}
|
|
2002
2244
|
});
|
|
2003
|
-
it("
|
|
2004
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
2245
|
+
it("does not warn on PreToolUse for grounded compatibility fallback code", async () => {
|
|
2246
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-grounded-"));
|
|
2005
2247
|
try {
|
|
2006
2248
|
const result = await dispatchCodexNativeHook({
|
|
2007
2249
|
hook_event_name: "PreToolUse",
|
|
2008
2250
|
cwd,
|
|
2009
2251
|
tool_name: "Bash",
|
|
2010
|
-
tool_use_id: "tool-
|
|
2011
|
-
tool_input: {
|
|
2252
|
+
tool_use_id: "tool-slop-grounded",
|
|
2253
|
+
tool_input: {
|
|
2254
|
+
command: [
|
|
2255
|
+
"cat > src/compat.ts <<'EOF'",
|
|
2256
|
+
"export function resolveCompatMode() {",
|
|
2257
|
+
" // temporary fallback because legacy compatibility needs fail-safe startup behavior",
|
|
2258
|
+
" return 'legacy';",
|
|
2259
|
+
"}",
|
|
2260
|
+
"// Tested: npm test",
|
|
2261
|
+
"EOF",
|
|
2262
|
+
].join("\n"),
|
|
2263
|
+
},
|
|
2012
2264
|
}, { cwd });
|
|
2013
2265
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2014
2266
|
assert.equal(result.outputJson, null);
|
|
@@ -2017,15 +2269,214 @@ esac
|
|
|
2017
2269
|
await rm(cwd, { recursive: true, force: true });
|
|
2018
2270
|
}
|
|
2019
2271
|
});
|
|
2020
|
-
it("blocks
|
|
2021
|
-
const cwd = await
|
|
2272
|
+
it("blocks Stop for untracked non-Bash-style sloppy fallback source edits", async () => {
|
|
2273
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-untracked-");
|
|
2274
|
+
try {
|
|
2275
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2276
|
+
await writeFile(join(cwd, "src", "runtime.ts"), [
|
|
2277
|
+
"export function loadRuntime() {",
|
|
2278
|
+
" // implement a quick hack fallback if it fails",
|
|
2279
|
+
" return process.env.RUNTIME || 'local';",
|
|
2280
|
+
"}",
|
|
2281
|
+
].join("\n"));
|
|
2282
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-untracked" }, { cwd });
|
|
2283
|
+
assert.equal(result.omxEventName, "stop");
|
|
2284
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2285
|
+
assert.equal(result.outputJson?.stopReason, "sloppy_fallback_diff_audit");
|
|
2286
|
+
assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
|
|
2287
|
+
assert.match(JSON.stringify(result.outputJson), /grounded design/);
|
|
2288
|
+
}
|
|
2289
|
+
finally {
|
|
2290
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
it("keeps blocking repeated Stop while sloppy fallback diff remains", async () => {
|
|
2294
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-repeat-");
|
|
2295
|
+
try {
|
|
2296
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2297
|
+
await writeFile(join(cwd, "src", "runtime.ts"), [
|
|
2298
|
+
"export function loadRuntime() {",
|
|
2299
|
+
" // implement a quick hack fallback if it fails",
|
|
2300
|
+
" return process.env.RUNTIME || 'local';",
|
|
2301
|
+
"}",
|
|
2302
|
+
].join("\n"));
|
|
2303
|
+
const payload = { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-repeat", turn_id: "turn-repeat" };
|
|
2304
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
2305
|
+
const repeated = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
|
|
2306
|
+
assert.equal(first.outputJson?.decision, "block");
|
|
2307
|
+
assert.equal(repeated.outputJson?.decision, "block");
|
|
2308
|
+
assert.equal(repeated.outputJson?.stopReason, "sloppy_fallback_diff_audit");
|
|
2309
|
+
}
|
|
2310
|
+
finally {
|
|
2311
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2312
|
+
}
|
|
2313
|
+
});
|
|
2314
|
+
it("blocks Stop for unstaged tracked sloppy fallback source edits", async () => {
|
|
2315
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-unstaged-");
|
|
2316
|
+
try {
|
|
2317
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2318
|
+
await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
|
|
2319
|
+
execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
|
|
2320
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
|
|
2321
|
+
await writeFile(join(cwd, "src", "runtime.ts"), [
|
|
2322
|
+
"export function loadRuntime() {",
|
|
2323
|
+
" // just bypass fallback if it fails",
|
|
2324
|
+
" return process.env.RUNTIME || 'local';",
|
|
2325
|
+
"}",
|
|
2326
|
+
].join("\n"));
|
|
2327
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-unstaged" }, { cwd });
|
|
2328
|
+
assert.equal(result.omxEventName, "stop");
|
|
2329
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2330
|
+
assert.match(JSON.stringify(result.outputJson), /unstaged/);
|
|
2331
|
+
}
|
|
2332
|
+
finally {
|
|
2333
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
it("blocks Stop from a subdirectory cwd for untracked sloppy source elsewhere", async () => {
|
|
2337
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-subdir-");
|
|
2338
|
+
try {
|
|
2339
|
+
await mkdir(join(cwd, "src", "nested"), { recursive: true });
|
|
2340
|
+
await writeFile(join(cwd, "src", "nested", "anchor.ts"), "export const anchor = true;\n");
|
|
2341
|
+
await writeFile(join(cwd, "src", "runtime.ts"), [
|
|
2342
|
+
"export function loadRuntime() {",
|
|
2343
|
+
" // implement a quick hack fallback if it fails",
|
|
2344
|
+
" return process.env.RUNTIME || 'local';",
|
|
2345
|
+
"}",
|
|
2346
|
+
].join("\n"));
|
|
2347
|
+
const subdir = join(cwd, "src", "nested");
|
|
2348
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd: subdir, session_id: "sess-stop-slop-subdir" }, { cwd: subdir });
|
|
2349
|
+
assert.equal(result.omxEventName, "stop");
|
|
2350
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2351
|
+
assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
|
|
2352
|
+
}
|
|
2353
|
+
finally {
|
|
2354
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
it("blocks Stop for staged sloppy fallback source edits", async () => {
|
|
2358
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-staged-");
|
|
2359
|
+
try {
|
|
2360
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2361
|
+
await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
|
|
2362
|
+
execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
|
|
2363
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
|
|
2364
|
+
await writeFile(join(cwd, "src", "runtime.ts"), [
|
|
2365
|
+
"export function loadRuntime() {",
|
|
2366
|
+
" // temporary workaround fallback if it fails",
|
|
2367
|
+
" return process.env.RUNTIME || 'local';",
|
|
2368
|
+
"}",
|
|
2369
|
+
].join("\n"));
|
|
2370
|
+
execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
|
|
2371
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-staged" }, { cwd });
|
|
2372
|
+
assert.equal(result.omxEventName, "stop");
|
|
2373
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2374
|
+
assert.match(JSON.stringify(result.outputJson), /staged/);
|
|
2375
|
+
}
|
|
2376
|
+
finally {
|
|
2377
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
it("does not block Stop for grounded compatibility fallback source edits", async () => {
|
|
2381
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-grounded-");
|
|
2382
|
+
try {
|
|
2383
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2384
|
+
await writeFile(join(cwd, "src", "compat.ts"), [
|
|
2385
|
+
"export function resolveCompatMode() {",
|
|
2386
|
+
" // temporary fallback for legacy startup",
|
|
2387
|
+
" // compatibility fail-safe tested by regression coverage",
|
|
2388
|
+
" return 'legacy';",
|
|
2389
|
+
"}",
|
|
2390
|
+
].join("\n"));
|
|
2391
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-grounded" }, { cwd });
|
|
2392
|
+
assert.equal(result.omxEventName, "stop");
|
|
2393
|
+
assert.equal(result.outputJson, null);
|
|
2394
|
+
}
|
|
2395
|
+
finally {
|
|
2396
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
it("does not block Stop when existing nearby source context grounds a new fallback line", async () => {
|
|
2400
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-existing-ground-");
|
|
2401
|
+
try {
|
|
2402
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2403
|
+
await writeFile(join(cwd, "src", "compat.ts"), [
|
|
2404
|
+
"export function resolveCompatMode() {",
|
|
2405
|
+
" // compatibility fail-safe tested by regression coverage",
|
|
2406
|
+
" return 'legacy';",
|
|
2407
|
+
"}",
|
|
2408
|
+
].join("\n"));
|
|
2409
|
+
execFileSync("git", ["add", "src/compat.ts"], { cwd, stdio: "ignore" });
|
|
2410
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
|
|
2411
|
+
await writeFile(join(cwd, "src", "compat.ts"), [
|
|
2412
|
+
"export function resolveCompatMode() {",
|
|
2413
|
+
" // compatibility fail-safe tested by regression coverage",
|
|
2414
|
+
" // temporary fallback if it fails",
|
|
2415
|
+
" return 'legacy';",
|
|
2416
|
+
"}",
|
|
2417
|
+
].join("\n"));
|
|
2418
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-existing-ground" }, { cwd });
|
|
2419
|
+
assert.equal(result.omxEventName, "stop");
|
|
2420
|
+
assert.equal(result.outputJson, null);
|
|
2421
|
+
}
|
|
2422
|
+
finally {
|
|
2423
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
it("does not block Stop for source-adjacent test file fallback wording", async () => {
|
|
2427
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-test-file-");
|
|
2428
|
+
try {
|
|
2429
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2430
|
+
await writeFile(join(cwd, "src", "runtime.test.ts"), "it('documents no quick hack fallback if it fails', () => {});\n");
|
|
2431
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-test-file" }, { cwd });
|
|
2432
|
+
assert.equal(result.omxEventName, "stop");
|
|
2433
|
+
assert.equal(result.outputJson, null);
|
|
2434
|
+
}
|
|
2435
|
+
finally {
|
|
2436
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
it("does not block Stop for docs-only fallback wording", async () => {
|
|
2440
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-docs-");
|
|
2441
|
+
try {
|
|
2442
|
+
await mkdir(join(cwd, "docs"), { recursive: true });
|
|
2443
|
+
await writeFile(join(cwd, "docs", "notes.md"), "Do not implement a quick hack fallback if it fails.\n");
|
|
2444
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-docs" }, { cwd });
|
|
2445
|
+
assert.equal(result.omxEventName, "stop");
|
|
2446
|
+
assert.equal(result.outputJson, null);
|
|
2447
|
+
}
|
|
2448
|
+
finally {
|
|
2449
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
it("keeps git commit Lore enforcement ahead of sloppy fallback advisory", async () => {
|
|
2453
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-git-priority-"));
|
|
2022
2454
|
try {
|
|
2023
2455
|
const result = await dispatchCodexNativeHook({
|
|
2024
2456
|
hook_event_name: "PreToolUse",
|
|
2025
2457
|
cwd,
|
|
2026
2458
|
tool_name: "Bash",
|
|
2027
|
-
tool_use_id: "tool-git-
|
|
2028
|
-
tool_input: { command: '
|
|
2459
|
+
tool_use_id: "tool-slop-git-priority",
|
|
2460
|
+
tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
|
|
2461
|
+
}, { cwd });
|
|
2462
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2463
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2464
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
2465
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /don't make potential slop/);
|
|
2466
|
+
}
|
|
2467
|
+
finally {
|
|
2468
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
|
|
2472
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
|
|
2473
|
+
try {
|
|
2474
|
+
const result = await dispatchCodexNativeHook({
|
|
2475
|
+
hook_event_name: "PreToolUse",
|
|
2476
|
+
cwd,
|
|
2477
|
+
tool_name: "Bash",
|
|
2478
|
+
tool_use_id: "tool-git-commit-invalid",
|
|
2479
|
+
tool_input: { command: 'git commit -m "fix tests"' },
|
|
2029
2480
|
}, { cwd });
|
|
2030
2481
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2031
2482
|
assert.deepEqual(result.outputJson, {
|
|
@@ -2042,13 +2493,292 @@ esac
|
|
|
2042
2493
|
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
2043
2494
|
].join("\n"),
|
|
2044
2495
|
});
|
|
2496
|
+
const hookSpecificOutput = result.outputJson
|
|
2497
|
+
.hookSpecificOutput ?? {};
|
|
2498
|
+
assert.equal("additionalContext" in hookSpecificOutput, false);
|
|
2045
2499
|
}
|
|
2046
2500
|
finally {
|
|
2047
2501
|
await rm(cwd, { recursive: true, force: true });
|
|
2048
2502
|
}
|
|
2049
2503
|
});
|
|
2050
|
-
it("
|
|
2051
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-
|
|
2504
|
+
it("allows non-Lore git commit messages when the Lore commit guard is explicitly disabled", async () => {
|
|
2505
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
|
|
2506
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
2507
|
+
try {
|
|
2508
|
+
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
2509
|
+
const result = await dispatchCodexNativeHook({
|
|
2510
|
+
hook_event_name: "PreToolUse",
|
|
2511
|
+
cwd,
|
|
2512
|
+
tool_name: "Bash",
|
|
2513
|
+
tool_use_id: "tool-git-commit-lore-disabled",
|
|
2514
|
+
tool_input: { command: 'git commit -m "fix: use conventional commit"' },
|
|
2515
|
+
}, { cwd });
|
|
2516
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2517
|
+
assert.equal(result.outputJson, null);
|
|
2518
|
+
}
|
|
2519
|
+
finally {
|
|
2520
|
+
if (original === undefined)
|
|
2521
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
2522
|
+
else
|
|
2523
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
2524
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
|
|
2528
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
|
|
2529
|
+
try {
|
|
2530
|
+
const result = await dispatchCodexNativeHook({
|
|
2531
|
+
hook_event_name: "PreToolUse",
|
|
2532
|
+
cwd,
|
|
2533
|
+
tool_name: "Bash",
|
|
2534
|
+
tool_use_id: "tool-git-commit-lore-inline-disabled",
|
|
2535
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
|
|
2536
|
+
}, { cwd });
|
|
2537
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2538
|
+
assert.equal(result.outputJson, null);
|
|
2539
|
+
}
|
|
2540
|
+
finally {
|
|
2541
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
it("does not treat newline-separated Lore guard assignment as inline git commit env", async () => {
|
|
2545
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
|
|
2546
|
+
try {
|
|
2547
|
+
const result = await dispatchCodexNativeHook({
|
|
2548
|
+
hook_event_name: "PreToolUse",
|
|
2549
|
+
cwd,
|
|
2550
|
+
tool_name: "Bash",
|
|
2551
|
+
tool_use_id: "tool-git-commit-lore-newline-assignment",
|
|
2552
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0\ngit commit -m "fix: conventional"' },
|
|
2553
|
+
}, { cwd });
|
|
2554
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2555
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2556
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
2557
|
+
}
|
|
2558
|
+
finally {
|
|
2559
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2560
|
+
}
|
|
2561
|
+
});
|
|
2562
|
+
it("restores default-on Lore guard when env -u unsets a disabled process env", async () => {
|
|
2563
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
|
|
2564
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
2565
|
+
try {
|
|
2566
|
+
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
2567
|
+
const result = await dispatchCodexNativeHook({
|
|
2568
|
+
hook_event_name: "PreToolUse",
|
|
2569
|
+
cwd,
|
|
2570
|
+
tool_name: "Bash",
|
|
2571
|
+
tool_use_id: "tool-git-commit-lore-env-unset",
|
|
2572
|
+
tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
|
|
2573
|
+
}, { cwd });
|
|
2574
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2575
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2576
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
2577
|
+
}
|
|
2578
|
+
finally {
|
|
2579
|
+
if (original === undefined)
|
|
2580
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
2581
|
+
else
|
|
2582
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
2583
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
it("restores default-on Lore guard when env -i clears a disabled process env", async () => {
|
|
2587
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
|
|
2588
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
2589
|
+
try {
|
|
2590
|
+
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
2591
|
+
const result = await dispatchCodexNativeHook({
|
|
2592
|
+
hook_event_name: "PreToolUse",
|
|
2593
|
+
cwd,
|
|
2594
|
+
tool_name: "Bash",
|
|
2595
|
+
tool_use_id: "tool-git-commit-lore-env-ignore",
|
|
2596
|
+
tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
|
|
2597
|
+
}, { cwd });
|
|
2598
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2599
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2600
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
2601
|
+
}
|
|
2602
|
+
finally {
|
|
2603
|
+
if (original === undefined)
|
|
2604
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
2605
|
+
else
|
|
2606
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
2607
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
it("keeps Lore commit enforcement enabled for unknown inline guard values", async () => {
|
|
2611
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
|
|
2612
|
+
try {
|
|
2613
|
+
const result = await dispatchCodexNativeHook({
|
|
2614
|
+
hook_event_name: "PreToolUse",
|
|
2615
|
+
cwd,
|
|
2616
|
+
tool_name: "Bash",
|
|
2617
|
+
tool_use_id: "tool-git-commit-lore-inline-unknown",
|
|
2618
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
|
|
2619
|
+
}, { cwd });
|
|
2620
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2621
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2622
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
2623
|
+
}
|
|
2624
|
+
finally {
|
|
2625
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
it("treats Lore commit guard disabled values as trim and case tolerant", async () => {
|
|
2629
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-off-"));
|
|
2630
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
2631
|
+
try {
|
|
2632
|
+
process.env.OMX_LORE_COMMIT_GUARD = " OFF ";
|
|
2633
|
+
const result = await dispatchCodexNativeHook({
|
|
2634
|
+
hook_event_name: "PreToolUse",
|
|
2635
|
+
cwd,
|
|
2636
|
+
tool_name: "Bash",
|
|
2637
|
+
tool_use_id: "tool-git-commit-lore-off",
|
|
2638
|
+
tool_input: { command: 'git commit -m "chore: conventional commit"' },
|
|
2639
|
+
}, { cwd });
|
|
2640
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2641
|
+
assert.equal(result.outputJson, null);
|
|
2642
|
+
}
|
|
2643
|
+
finally {
|
|
2644
|
+
if (original === undefined)
|
|
2645
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
2646
|
+
else
|
|
2647
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
2648
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
it("keeps Lore commit enforcement enabled for unknown guard values", async () => {
|
|
2652
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
|
|
2653
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
2654
|
+
try {
|
|
2655
|
+
process.env.OMX_LORE_COMMIT_GUARD = "maybe";
|
|
2656
|
+
const result = await dispatchCodexNativeHook({
|
|
2657
|
+
hook_event_name: "PreToolUse",
|
|
2658
|
+
cwd,
|
|
2659
|
+
tool_name: "Bash",
|
|
2660
|
+
tool_use_id: "tool-git-commit-lore-unknown",
|
|
2661
|
+
tool_input: { command: 'git commit -m "fix tests"' },
|
|
2662
|
+
}, { cwd });
|
|
2663
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2664
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2665
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
2666
|
+
}
|
|
2667
|
+
finally {
|
|
2668
|
+
if (original === undefined)
|
|
2669
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
2670
|
+
else
|
|
2671
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
2672
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
it("continues to later PreToolUse checks when Lore commit guard is disabled", async () => {
|
|
2676
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-lore-disabled-destructive-"));
|
|
2677
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
2678
|
+
try {
|
|
2679
|
+
process.env.OMX_LORE_COMMIT_GUARD = "false";
|
|
2680
|
+
const result = await dispatchCodexNativeHook({
|
|
2681
|
+
hook_event_name: "PreToolUse",
|
|
2682
|
+
cwd,
|
|
2683
|
+
tool_name: "Bash",
|
|
2684
|
+
tool_use_id: "tool-lore-disabled-destructive",
|
|
2685
|
+
tool_input: { command: "rm -rf dist" },
|
|
2686
|
+
}, { cwd });
|
|
2687
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2688
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
2689
|
+
assert.match(JSON.stringify(result.outputJson), /Destructive Bash command detected/);
|
|
2690
|
+
}
|
|
2691
|
+
finally {
|
|
2692
|
+
if (original === undefined)
|
|
2693
|
+
delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
2694
|
+
else
|
|
2695
|
+
process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
2696
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
it("stays silent on PreToolUse for `git help commit`", async () => {
|
|
2700
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
|
|
2701
|
+
try {
|
|
2702
|
+
const result = await dispatchCodexNativeHook({
|
|
2703
|
+
hook_event_name: "PreToolUse",
|
|
2704
|
+
cwd,
|
|
2705
|
+
tool_name: "Bash",
|
|
2706
|
+
tool_use_id: "tool-git-help-commit",
|
|
2707
|
+
tool_input: { command: "git help commit" },
|
|
2708
|
+
}, { cwd });
|
|
2709
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2710
|
+
assert.equal(result.outputJson, null);
|
|
2711
|
+
}
|
|
2712
|
+
finally {
|
|
2713
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2714
|
+
}
|
|
2715
|
+
});
|
|
2716
|
+
it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
|
|
2717
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
|
|
2718
|
+
try {
|
|
2719
|
+
const result = await dispatchCodexNativeHook({
|
|
2720
|
+
hook_event_name: "PreToolUse",
|
|
2721
|
+
cwd,
|
|
2722
|
+
tool_name: "Bash",
|
|
2723
|
+
tool_use_id: "tool-git-config-alias-commit",
|
|
2724
|
+
tool_input: { command: "git config alias.ci commit" },
|
|
2725
|
+
}, { cwd });
|
|
2726
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2727
|
+
assert.equal(result.outputJson, null);
|
|
2728
|
+
}
|
|
2729
|
+
finally {
|
|
2730
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
2733
|
+
it("stays silent on PreToolUse for `git tag commit`", async () => {
|
|
2734
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
|
|
2735
|
+
try {
|
|
2736
|
+
const result = await dispatchCodexNativeHook({
|
|
2737
|
+
hook_event_name: "PreToolUse",
|
|
2738
|
+
cwd,
|
|
2739
|
+
tool_name: "Bash",
|
|
2740
|
+
tool_use_id: "tool-git-tag-commit",
|
|
2741
|
+
tool_input: { command: "git tag commit" },
|
|
2742
|
+
}, { cwd });
|
|
2743
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2744
|
+
assert.equal(result.outputJson, null);
|
|
2745
|
+
}
|
|
2746
|
+
finally {
|
|
2747
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
it("blocks PreToolUse env-prefixed git commit when the inline message is not Lore-compliant", async () => {
|
|
2751
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-env-invalid-"));
|
|
2752
|
+
try {
|
|
2753
|
+
const result = await dispatchCodexNativeHook({
|
|
2754
|
+
hook_event_name: "PreToolUse",
|
|
2755
|
+
cwd,
|
|
2756
|
+
tool_name: "Bash",
|
|
2757
|
+
tool_use_id: "tool-git-commit-env-invalid",
|
|
2758
|
+
tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
|
|
2759
|
+
}, { cwd });
|
|
2760
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2761
|
+
assert.deepEqual(result.outputJson, {
|
|
2762
|
+
decision: "block",
|
|
2763
|
+
reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
2764
|
+
hookSpecificOutput: {
|
|
2765
|
+
hookEventName: "PreToolUse",
|
|
2766
|
+
},
|
|
2767
|
+
systemMessage: [
|
|
2768
|
+
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
2769
|
+
"- Add a blank line after the subject before the narrative body.",
|
|
2770
|
+
"- Add a narrative body paragraph explaining the decision context.",
|
|
2771
|
+
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
2772
|
+
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
2773
|
+
].join("\n"),
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
finally {
|
|
2777
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
it("blocks PreToolUse git commit when git options appear before the real commit subcommand", async () => {
|
|
2781
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-option-invalid-"));
|
|
2052
2782
|
try {
|
|
2053
2783
|
const result = await dispatchCodexNativeHook({
|
|
2054
2784
|
hook_event_name: "PreToolUse",
|
|
@@ -2956,12 +3686,44 @@ esac
|
|
|
2956
3686
|
await rm(cwd, { recursive: true, force: true });
|
|
2957
3687
|
}
|
|
2958
3688
|
});
|
|
3689
|
+
for (const rootActiveCase of [
|
|
3690
|
+
{ mode: "autopilot", phase: "execution" },
|
|
3691
|
+
{ mode: "ultrawork", phase: "executing" },
|
|
3692
|
+
{ mode: "ultraqa", phase: "diagnose" },
|
|
3693
|
+
]) {
|
|
3694
|
+
it(`returns Stop continuation output from root ${rootActiveCase.mode} state when no session is active`, async () => {
|
|
3695
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-root-${rootActiveCase.mode}-`));
|
|
3696
|
+
try {
|
|
3697
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3698
|
+
await mkdir(stateDir, { recursive: true });
|
|
3699
|
+
await writeJson(join(stateDir, `${rootActiveCase.mode}-state.json`), {
|
|
3700
|
+
active: true,
|
|
3701
|
+
mode: rootActiveCase.mode,
|
|
3702
|
+
current_phase: rootActiveCase.phase,
|
|
3703
|
+
});
|
|
3704
|
+
const result = await dispatchCodexNativeHook({
|
|
3705
|
+
hook_event_name: "Stop",
|
|
3706
|
+
cwd,
|
|
3707
|
+
}, { cwd });
|
|
3708
|
+
assert.equal(result.omxEventName, "stop");
|
|
3709
|
+
assert.deepEqual(result.outputJson, {
|
|
3710
|
+
decision: "block",
|
|
3711
|
+
reason: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}); continue the task and gather fresh verification evidence before stopping.`,
|
|
3712
|
+
stopReason: `${rootActiveCase.mode}_${rootActiveCase.phase}`,
|
|
3713
|
+
systemMessage: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}).`,
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
finally {
|
|
3717
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3718
|
+
}
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
2959
3721
|
it("returns Stop continuation output while Autopilot is active", async () => {
|
|
2960
3722
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-"));
|
|
2961
3723
|
try {
|
|
2962
3724
|
const stateDir = join(cwd, ".omx", "state");
|
|
2963
|
-
await mkdir(stateDir, { recursive: true });
|
|
2964
|
-
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
3725
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autopilot"), { recursive: true });
|
|
3726
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autopilot", "autopilot-state.json"), {
|
|
2965
3727
|
active: true,
|
|
2966
3728
|
current_phase: "execution",
|
|
2967
3729
|
});
|
|
@@ -2986,8 +3748,8 @@ esac
|
|
|
2986
3748
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
|
|
2987
3749
|
try {
|
|
2988
3750
|
const stateDir = join(cwd, ".omx", "state");
|
|
2989
|
-
await mkdir(stateDir, { recursive: true });
|
|
2990
|
-
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
3751
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay"), { recursive: true });
|
|
3752
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay", "autopilot-state.json"), {
|
|
2991
3753
|
active: true,
|
|
2992
3754
|
current_phase: "planning",
|
|
2993
3755
|
});
|
|
@@ -3041,12 +3803,40 @@ esac
|
|
|
3041
3803
|
await rm(cwd, { recursive: true, force: true });
|
|
3042
3804
|
}
|
|
3043
3805
|
});
|
|
3806
|
+
for (const staleRootCase of [
|
|
3807
|
+
{ mode: "autopilot", phase: "execution" },
|
|
3808
|
+
{ mode: "ultrawork", phase: "executing" },
|
|
3809
|
+
{ mode: "ultraqa", phase: "diagnose" },
|
|
3810
|
+
]) {
|
|
3811
|
+
it(`does not block Stop from stale root ${staleRootCase.mode} state when the explicit session directory is missing`, async () => {
|
|
3812
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-missing-session-${staleRootCase.mode}-`));
|
|
3813
|
+
try {
|
|
3814
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3815
|
+
await mkdir(stateDir, { recursive: true });
|
|
3816
|
+
await writeJson(join(stateDir, `${staleRootCase.mode}-state.json`), {
|
|
3817
|
+
active: true,
|
|
3818
|
+
mode: staleRootCase.mode,
|
|
3819
|
+
current_phase: staleRootCase.phase,
|
|
3820
|
+
});
|
|
3821
|
+
const result = await dispatchCodexNativeHook({
|
|
3822
|
+
hook_event_name: "Stop",
|
|
3823
|
+
cwd,
|
|
3824
|
+
session_id: "missing-session",
|
|
3825
|
+
}, { cwd });
|
|
3826
|
+
assert.equal(result.omxEventName, "stop");
|
|
3827
|
+
assert.equal(result.outputJson, null);
|
|
3828
|
+
}
|
|
3829
|
+
finally {
|
|
3830
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3044
3834
|
it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
|
|
3045
3835
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
|
|
3046
3836
|
try {
|
|
3047
3837
|
const stateDir = join(cwd, ".omx", "state");
|
|
3048
|
-
await mkdir(stateDir, { recursive: true });
|
|
3049
|
-
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
3838
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome"), { recursive: true });
|
|
3839
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome", "autopilot-state.json"), {
|
|
3050
3840
|
active: true,
|
|
3051
3841
|
current_phase: "execution",
|
|
3052
3842
|
run_outcome: "blocked_on_user",
|
|
@@ -3067,8 +3857,8 @@ esac
|
|
|
3067
3857
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
|
|
3068
3858
|
try {
|
|
3069
3859
|
const stateDir = join(cwd, ".omx", "state");
|
|
3070
|
-
await mkdir(stateDir, { recursive: true });
|
|
3071
|
-
await writeJson(join(stateDir, "ultrawork-state.json"), {
|
|
3860
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork"), { recursive: true });
|
|
3861
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork", "ultrawork-state.json"), {
|
|
3072
3862
|
active: true,
|
|
3073
3863
|
current_phase: "executing",
|
|
3074
3864
|
});
|
|
@@ -3088,8 +3878,8 @@ esac
|
|
|
3088
3878
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultraqa-"));
|
|
3089
3879
|
try {
|
|
3090
3880
|
const stateDir = join(cwd, ".omx", "state");
|
|
3091
|
-
await mkdir(stateDir, { recursive: true });
|
|
3092
|
-
await writeJson(join(stateDir, "ultraqa-state.json"), {
|
|
3881
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultraqa"), { recursive: true });
|
|
3882
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultraqa", "ultraqa-state.json"), {
|
|
3093
3883
|
active: true,
|
|
3094
3884
|
current_phase: "diagnose",
|
|
3095
3885
|
});
|
|
@@ -3105,16 +3895,42 @@ esac
|
|
|
3105
3895
|
await rm(cwd, { recursive: true, force: true });
|
|
3106
3896
|
}
|
|
3107
3897
|
});
|
|
3108
|
-
it("
|
|
3109
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-"));
|
|
3898
|
+
it("marks leader-owned team attention during native Stop dispatch without a polling watcher", async () => {
|
|
3899
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-attention-"));
|
|
3110
3900
|
try {
|
|
3111
|
-
|
|
3112
|
-
await
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3901
|
+
await initTeamState("stop-attention-team", "native stop attention", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-attention" });
|
|
3902
|
+
const result = await dispatchCodexNativeHook({
|
|
3903
|
+
hook_event_name: "Stop",
|
|
3904
|
+
cwd,
|
|
3905
|
+
session_id: "sess-stop-team-attention",
|
|
3906
|
+
}, { cwd });
|
|
3907
|
+
const attention = await readTeamLeaderAttention("stop-attention-team", cwd);
|
|
3908
|
+
assert.equal(result.omxEventName, "stop");
|
|
3909
|
+
assert.equal(attention?.source, "native_stop");
|
|
3910
|
+
assert.equal(attention?.leader_session_active, false);
|
|
3911
|
+
assert.equal(attention?.leader_session_id, "sess-stop-team-attention");
|
|
3912
|
+
assert.match(attention?.leader_session_stopped_at ?? "", /^\d{4}-\d{2}-\d{2}T/);
|
|
3913
|
+
assert.deepEqual(result.outputJson, {
|
|
3914
|
+
decision: "block",
|
|
3915
|
+
reason: `OMX team pipeline is still active (stop-attention-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
3916
|
+
stopReason: "team_team-exec",
|
|
3917
|
+
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
finally {
|
|
3921
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
3924
|
+
it("returns Stop continuation output while team phase is non-terminal", async () => {
|
|
3925
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-"));
|
|
3926
|
+
try {
|
|
3927
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3928
|
+
await mkdir(stateDir, { recursive: true });
|
|
3929
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
3930
|
+
active: true,
|
|
3931
|
+
current_phase: "team-exec",
|
|
3932
|
+
team_name: "review-team",
|
|
3933
|
+
});
|
|
3118
3934
|
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
3119
3935
|
current_phase: "team-verify",
|
|
3120
3936
|
max_fix_attempts: 3,
|
|
@@ -3158,7 +3974,7 @@ esac
|
|
|
3158
3974
|
team_state_root: join(cwd, ".omx", "state"),
|
|
3159
3975
|
});
|
|
3160
3976
|
await writeJson(join(workerDir, "status.json"), {
|
|
3161
|
-
state: "
|
|
3977
|
+
state: "working",
|
|
3162
3978
|
current_task_id: "1",
|
|
3163
3979
|
updated_at: new Date().toISOString(),
|
|
3164
3980
|
});
|
|
@@ -3201,7 +4017,69 @@ esac
|
|
|
3201
4017
|
await rm(cwd, { recursive: true, force: true });
|
|
3202
4018
|
}
|
|
3203
4019
|
});
|
|
3204
|
-
it("
|
|
4020
|
+
it("blocks Stop as a team-worker task failure when worker status is terminal but task evidence is not completed", async () => {
|
|
4021
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-stale-"));
|
|
4022
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4023
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4024
|
+
const prevLeaderCwd = process.env.OMX_TEAM_LEADER_CWD;
|
|
4025
|
+
try {
|
|
4026
|
+
await initTeamState("worker-stale-team", "worker stale stop fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-stale" });
|
|
4027
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4028
|
+
const workerCwd = join(cwd, ".omx", "team", "worker-stale-team", "worktrees", "worker-1");
|
|
4029
|
+
const workerDir = join(stateDir, "team", "worker-stale-team", "workers", "worker-1");
|
|
4030
|
+
await mkdir(workerCwd, { recursive: true });
|
|
4031
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4032
|
+
name: "worker-1",
|
|
4033
|
+
index: 1,
|
|
4034
|
+
role: "executor",
|
|
4035
|
+
assigned_tasks: ["1"],
|
|
4036
|
+
worktree_path: workerCwd,
|
|
4037
|
+
team_state_root: stateDir,
|
|
4038
|
+
});
|
|
4039
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
4040
|
+
state: "done",
|
|
4041
|
+
current_task_id: "1",
|
|
4042
|
+
updated_at: new Date().toISOString(),
|
|
4043
|
+
});
|
|
4044
|
+
await writeJson(join(stateDir, "team", "worker-stale-team", "tasks", "task-1.json"), {
|
|
4045
|
+
id: "1",
|
|
4046
|
+
subject: "stale hook task",
|
|
4047
|
+
description: "non-completed task should still block terminal worker Stop",
|
|
4048
|
+
status: "in_progress",
|
|
4049
|
+
owner: "worker-1",
|
|
4050
|
+
created_at: new Date().toISOString(),
|
|
4051
|
+
});
|
|
4052
|
+
process.env.OMX_TEAM_WORKER = "worker-stale-team/worker-1";
|
|
4053
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4054
|
+
process.env.OMX_TEAM_LEADER_CWD = cwd;
|
|
4055
|
+
const payload = {
|
|
4056
|
+
hook_event_name: "Stop",
|
|
4057
|
+
cwd: workerCwd,
|
|
4058
|
+
session_id: "sess-stop-team-worker-stale",
|
|
4059
|
+
thread_id: "thread-stop-team-worker-stale",
|
|
4060
|
+
};
|
|
4061
|
+
const result = await dispatchCodexNativeHook(payload, { cwd: workerCwd });
|
|
4062
|
+
const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd: workerCwd });
|
|
4063
|
+
assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_1_in_progress");
|
|
4064
|
+
assert.equal(replay.outputJson, null);
|
|
4065
|
+
}
|
|
4066
|
+
finally {
|
|
4067
|
+
if (typeof prevTeamWorker === "string")
|
|
4068
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4069
|
+
else
|
|
4070
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
4071
|
+
if (typeof prevTeamStateRoot === "string")
|
|
4072
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4073
|
+
else
|
|
4074
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4075
|
+
if (typeof prevLeaderCwd === "string")
|
|
4076
|
+
process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
|
|
4077
|
+
else
|
|
4078
|
+
delete process.env.OMX_TEAM_LEADER_CWD;
|
|
4079
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4080
|
+
}
|
|
4081
|
+
});
|
|
4082
|
+
it("re-blocks live team worker Stop replays but suppresses stale terminal worker repeats", async () => {
|
|
3205
4083
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
|
|
3206
4084
|
try {
|
|
3207
4085
|
await initTeamState("worker-repeat-team", "worker stop repeat guard", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-repeat" });
|
|
@@ -3219,7 +4097,7 @@ esac
|
|
|
3219
4097
|
team_state_root: stateDir,
|
|
3220
4098
|
});
|
|
3221
4099
|
await writeJson(join(workerDir, "status.json"), {
|
|
3222
|
-
state: "
|
|
4100
|
+
state: "working",
|
|
3223
4101
|
current_task_id: "1",
|
|
3224
4102
|
updated_at: new Date().toISOString(),
|
|
3225
4103
|
});
|
|
@@ -3259,9 +4137,9 @@ esac
|
|
|
3259
4137
|
owner: "worker-1",
|
|
3260
4138
|
created_at: new Date().toISOString(),
|
|
3261
4139
|
});
|
|
3262
|
-
const stateChanged = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-
|
|
4140
|
+
const stateChanged = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-3", stop_hook_active: true }, { cwd: workerCwd });
|
|
3263
4141
|
assert.deepEqual(first.outputJson, expectedInProgress);
|
|
3264
|
-
assert.deepEqual(replay.outputJson,
|
|
4142
|
+
assert.deepEqual(replay.outputJson, expectedInProgress);
|
|
3265
4143
|
assert.deepEqual(freshTurn.outputJson, expectedInProgress);
|
|
3266
4144
|
assert.deepEqual(stateChanged.outputJson, {
|
|
3267
4145
|
decision: "block",
|
|
@@ -3274,13 +4152,39 @@ esac
|
|
|
3274
4152
|
await rm(cwd, { recursive: true, force: true });
|
|
3275
4153
|
}
|
|
3276
4154
|
});
|
|
3277
|
-
it("
|
|
4155
|
+
it("allows Stop for a team worker when assigned task is terminal and bypasses generic team blocking", async () => {
|
|
3278
4156
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
|
|
3279
4157
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
3280
4158
|
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4159
|
+
const prevPath = process.env.PATH;
|
|
3281
4160
|
try {
|
|
3282
4161
|
await initTeamState("worker-stop-team-terminal", "worker stop terminal fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" });
|
|
4162
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
4163
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
4164
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
4165
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
4166
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
3283
4167
|
const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
|
|
4168
|
+
await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "config.json"), {
|
|
4169
|
+
name: "worker-stop-team-terminal",
|
|
4170
|
+
tmux_session: "omx-team-worker-stop",
|
|
4171
|
+
leader_pane_id: "%42",
|
|
4172
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
4173
|
+
});
|
|
4174
|
+
await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "manifest.v2.json"), {
|
|
4175
|
+
name: "worker-stop-team-terminal",
|
|
4176
|
+
tmux_session: "omx-team-worker-stop",
|
|
4177
|
+
leader_pane_id: "%42",
|
|
4178
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
4179
|
+
});
|
|
4180
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4181
|
+
name: "worker-1",
|
|
4182
|
+
index: 1,
|
|
4183
|
+
role: "executor",
|
|
4184
|
+
assigned_tasks: ["1"],
|
|
4185
|
+
worktree_path: cwd,
|
|
4186
|
+
team_state_root: join(cwd, ".omx", "state"),
|
|
4187
|
+
});
|
|
3284
4188
|
await writeJson(join(workerDir, "status.json"), {
|
|
3285
4189
|
state: "done",
|
|
3286
4190
|
current_task_id: "1",
|
|
@@ -3296,27 +4200,424 @@ esac
|
|
|
3296
4200
|
});
|
|
3297
4201
|
process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
|
|
3298
4202
|
process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
|
|
4203
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
3299
4204
|
const result = await dispatchCodexNativeHook({
|
|
3300
4205
|
hook_event_name: "Stop",
|
|
3301
4206
|
cwd,
|
|
3302
4207
|
session_id: "sess-stop-team-worker-terminal",
|
|
3303
4208
|
}, { cwd });
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
4209
|
+
const replay = await dispatchCodexNativeHook({
|
|
4210
|
+
hook_event_name: "Stop",
|
|
4211
|
+
cwd,
|
|
4212
|
+
session_id: "sess-stop-team-worker-terminal",
|
|
4213
|
+
turn_id: "turn-worker-stop-terminal-replay",
|
|
4214
|
+
}, { cwd });
|
|
4215
|
+
assert.equal(result.outputJson, null);
|
|
4216
|
+
assert.equal(replay.outputJson, null);
|
|
4217
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
4218
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/g) || [];
|
|
4219
|
+
assert.equal(stopNudges.length, 1, "allowed worker Stop should nudge leader exactly once inside cooldown");
|
|
4220
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
4221
|
+
assert.equal(nudgeState.delivery, "sent");
|
|
4222
|
+
}
|
|
4223
|
+
finally {
|
|
4224
|
+
if (typeof prevTeamWorker === "string")
|
|
4225
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4226
|
+
else
|
|
4227
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
4228
|
+
if (typeof prevTeamStateRoot === "string")
|
|
4229
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4230
|
+
else
|
|
4231
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4232
|
+
if (typeof prevPath === "string")
|
|
4233
|
+
process.env.PATH = prevPath;
|
|
4234
|
+
else
|
|
4235
|
+
delete process.env.PATH;
|
|
4236
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4237
|
+
}
|
|
4238
|
+
});
|
|
4239
|
+
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
4240
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
4241
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4242
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4243
|
+
const prevPath = process.env.PATH;
|
|
4244
|
+
try {
|
|
4245
|
+
await initTeamState("worker-stop-helper-fail", "worker stop helper failure", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" });
|
|
4246
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
4247
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
4248
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
|
|
4249
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
4250
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4251
|
+
const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
|
|
4252
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
|
|
4253
|
+
name: "worker-stop-helper-fail",
|
|
4254
|
+
tmux_session: "omx-team-worker-stop",
|
|
4255
|
+
leader_pane_id: "%42",
|
|
4256
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
4257
|
+
});
|
|
4258
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
|
|
4259
|
+
name: "worker-stop-helper-fail",
|
|
4260
|
+
tmux_session: "omx-team-worker-stop",
|
|
4261
|
+
leader_pane_id: "%42",
|
|
4262
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
3309
4263
|
});
|
|
4264
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4265
|
+
name: "worker-1",
|
|
4266
|
+
assigned_tasks: ["1"],
|
|
4267
|
+
team_state_root: stateDir,
|
|
4268
|
+
});
|
|
4269
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
4270
|
+
state: "done",
|
|
4271
|
+
current_task_id: "1",
|
|
4272
|
+
updated_at: new Date().toISOString(),
|
|
4273
|
+
});
|
|
4274
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
|
|
4275
|
+
id: "1",
|
|
4276
|
+
status: "completed",
|
|
4277
|
+
owner: "worker-1",
|
|
4278
|
+
});
|
|
4279
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
|
|
4280
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4281
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
4282
|
+
const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" }, { cwd });
|
|
4283
|
+
assert.equal(result.outputJson, null);
|
|
4284
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
4285
|
+
assert.equal(nudgeState.delivery, "deferred");
|
|
4286
|
+
}
|
|
4287
|
+
finally {
|
|
4288
|
+
if (typeof prevTeamWorker === "string")
|
|
4289
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4290
|
+
else
|
|
4291
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
4292
|
+
if (typeof prevTeamStateRoot === "string")
|
|
4293
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4294
|
+
else
|
|
4295
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4296
|
+
if (typeof prevPath === "string")
|
|
4297
|
+
process.env.PATH = prevPath;
|
|
4298
|
+
else
|
|
4299
|
+
delete process.env.PATH;
|
|
4300
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
|
|
4304
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
|
|
4305
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4306
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4307
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4308
|
+
const prevPath = process.env.PATH;
|
|
4309
|
+
try {
|
|
4310
|
+
await initTeamState("worker-stop-failed-task", "worker stop failed task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" });
|
|
4311
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
4312
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
4313
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
4314
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
4315
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
4316
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4317
|
+
const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
|
|
4318
|
+
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
|
|
4319
|
+
name: "worker-stop-failed-task",
|
|
4320
|
+
tmux_session: "omx-team-worker-stop",
|
|
4321
|
+
leader_pane_id: "%42",
|
|
4322
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
4323
|
+
});
|
|
4324
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4325
|
+
name: "worker-1",
|
|
4326
|
+
assigned_tasks: ["1"],
|
|
4327
|
+
team_state_root: stateDir,
|
|
4328
|
+
});
|
|
4329
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
4330
|
+
state: "failed",
|
|
4331
|
+
current_task_id: "1",
|
|
4332
|
+
updated_at: new Date().toISOString(),
|
|
4333
|
+
});
|
|
4334
|
+
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
|
|
4335
|
+
id: "1",
|
|
4336
|
+
status: "failed",
|
|
4337
|
+
owner: "worker-1",
|
|
4338
|
+
});
|
|
4339
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
|
|
4340
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4341
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4342
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
4343
|
+
const result = await dispatchCodexNativeHook({
|
|
4344
|
+
hook_event_name: "Stop",
|
|
4345
|
+
cwd,
|
|
4346
|
+
session_id: "sess-stop-team-worker-failed",
|
|
4347
|
+
thread_id: "thread-stop-team-worker-failed",
|
|
4348
|
+
turn_id: "turn-stop-team-worker-failed",
|
|
4349
|
+
}, { cwd });
|
|
4350
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
4351
|
+
assert.match(String(result.outputJson?.stopReason || ""), /non_completed_task_1_failed/);
|
|
4352
|
+
assert.match(JSON.stringify(result.outputJson), /team/i);
|
|
4353
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
|
|
4354
|
+
const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
|
|
4355
|
+
assert.doesNotMatch(tmuxLog, /native Stop allowed/);
|
|
4356
|
+
}
|
|
4357
|
+
finally {
|
|
4358
|
+
if (typeof prevTeamWorker === "string")
|
|
4359
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4360
|
+
else
|
|
4361
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
4362
|
+
if (typeof prevInternalTeamWorker === "string")
|
|
4363
|
+
process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
4364
|
+
else
|
|
4365
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4366
|
+
if (typeof prevTeamStateRoot === "string")
|
|
4367
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4368
|
+
else
|
|
4369
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4370
|
+
if (typeof prevPath === "string")
|
|
4371
|
+
process.env.PATH = prevPath;
|
|
4372
|
+
else
|
|
4373
|
+
delete process.env.PATH;
|
|
4374
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4375
|
+
}
|
|
4376
|
+
});
|
|
4377
|
+
it("blocks worker Stop on missing task assignment without relying on generic team state", async () => {
|
|
4378
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-assignment-"));
|
|
4379
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4380
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4381
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4382
|
+
try {
|
|
4383
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4384
|
+
const workerDir = join(stateDir, "team", "worker-missing-assignment", "workers", "worker-1");
|
|
4385
|
+
await mkdir(workerDir, { recursive: true });
|
|
4386
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4387
|
+
name: "worker-1",
|
|
4388
|
+
assigned_tasks: [],
|
|
4389
|
+
team_state_root: stateDir,
|
|
4390
|
+
});
|
|
4391
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
4392
|
+
state: "idle",
|
|
4393
|
+
updated_at: new Date().toISOString(),
|
|
4394
|
+
});
|
|
4395
|
+
process.env.OMX_TEAM_WORKER = "worker-missing-assignment/worker-1";
|
|
4396
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4397
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4398
|
+
const result = await dispatchCodexNativeHook({
|
|
4399
|
+
hook_event_name: "Stop",
|
|
4400
|
+
cwd,
|
|
4401
|
+
session_id: "sess-stop-team-worker-missing-assignment",
|
|
4402
|
+
thread_id: "thread-stop-team-worker-missing-assignment",
|
|
4403
|
+
turn_id: "turn-stop-team-worker-missing-assignment",
|
|
4404
|
+
}, { cwd });
|
|
4405
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
4406
|
+
assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_task_assignment");
|
|
4407
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
|
|
4408
|
+
}
|
|
4409
|
+
finally {
|
|
4410
|
+
if (typeof prevTeamWorker === "string")
|
|
4411
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4412
|
+
else
|
|
4413
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
4414
|
+
if (typeof prevInternalTeamWorker === "string")
|
|
4415
|
+
process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
4416
|
+
else
|
|
4417
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4418
|
+
if (typeof prevTeamStateRoot === "string")
|
|
4419
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4420
|
+
else
|
|
4421
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4422
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4423
|
+
}
|
|
4424
|
+
});
|
|
4425
|
+
it("blocks unresolved worker Stop before generic auto-nudge can bypass it", async () => {
|
|
4426
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-state-"));
|
|
4427
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4428
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4429
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4430
|
+
const prevPath = process.env.PATH;
|
|
4431
|
+
try {
|
|
4432
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4433
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
4434
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
4435
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
4436
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
4437
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
4438
|
+
process.env.OMX_TEAM_WORKER = "worker-missing-state/worker-1";
|
|
4439
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4440
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4441
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
4442
|
+
const result = await dispatchCodexNativeHook({
|
|
4443
|
+
hook_event_name: "Stop",
|
|
4444
|
+
cwd,
|
|
4445
|
+
session_id: "sess-stop-team-worker-missing-state",
|
|
4446
|
+
thread_id: "thread-stop-team-worker-missing-state",
|
|
4447
|
+
turn_id: "turn-stop-team-worker-missing-state",
|
|
4448
|
+
last_assistant_message: "Should I proceed?",
|
|
4449
|
+
}, { cwd });
|
|
4450
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
4451
|
+
assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_worker_state");
|
|
4452
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /auto_nudge/);
|
|
4453
|
+
const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
|
|
4454
|
+
assert.doesNotMatch(tmuxLog, /native Stop allowed/);
|
|
4455
|
+
}
|
|
4456
|
+
finally {
|
|
4457
|
+
if (typeof prevTeamWorker === "string")
|
|
4458
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4459
|
+
else
|
|
4460
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
4461
|
+
if (typeof prevInternalTeamWorker === "string")
|
|
4462
|
+
process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
4463
|
+
else
|
|
4464
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4465
|
+
if (typeof prevTeamStateRoot === "string")
|
|
4466
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4467
|
+
else
|
|
4468
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4469
|
+
if (typeof prevPath === "string")
|
|
4470
|
+
process.env.PATH = prevPath;
|
|
4471
|
+
else
|
|
4472
|
+
delete process.env.PATH;
|
|
4473
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4474
|
+
}
|
|
4475
|
+
});
|
|
4476
|
+
it("prefers canonical internal worker identity over public worker identity for Stop nudges", async () => {
|
|
4477
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-internal-env-"));
|
|
4478
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4479
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4480
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4481
|
+
const prevPath = process.env.PATH;
|
|
4482
|
+
try {
|
|
4483
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4484
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
4485
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
4486
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
4487
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
4488
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
4489
|
+
const workerDir = join(stateDir, "team", "internal-stop-team", "workers", "worker-1");
|
|
4490
|
+
await writeJson(join(stateDir, "team", "internal-stop-team", "config.json"), {
|
|
4491
|
+
name: "internal-stop-team",
|
|
4492
|
+
tmux_session: "omx-team-worker-stop",
|
|
4493
|
+
leader_pane_id: "%42",
|
|
4494
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
4495
|
+
});
|
|
4496
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4497
|
+
name: "worker-1",
|
|
4498
|
+
assigned_tasks: ["1"],
|
|
4499
|
+
team_state_root: stateDir,
|
|
4500
|
+
});
|
|
4501
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
4502
|
+
state: "done",
|
|
4503
|
+
current_task_id: "1",
|
|
4504
|
+
updated_at: new Date().toISOString(),
|
|
4505
|
+
});
|
|
4506
|
+
await writeJson(join(stateDir, "team", "internal-stop-team", "tasks", "task-1.json"), {
|
|
4507
|
+
id: "1",
|
|
4508
|
+
status: "completed",
|
|
4509
|
+
owner: "worker-1",
|
|
4510
|
+
});
|
|
4511
|
+
process.env.OMX_TEAM_WORKER = "public-stop-team/worker-1";
|
|
4512
|
+
process.env.OMX_TEAM_INTERNAL_WORKER = "internal-stop-team/worker-1";
|
|
4513
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4514
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
4515
|
+
const result = await dispatchCodexNativeHook({
|
|
4516
|
+
hook_event_name: "Stop",
|
|
4517
|
+
cwd,
|
|
4518
|
+
session_id: "sess-stop-team-worker-internal-env",
|
|
4519
|
+
thread_id: "thread-stop-team-worker-internal-env",
|
|
4520
|
+
turn_id: "turn-stop-team-worker-internal-env",
|
|
4521
|
+
}, { cwd });
|
|
4522
|
+
assert.equal(result.outputJson, null);
|
|
4523
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
4524
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
4525
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), true);
|
|
4526
|
+
}
|
|
4527
|
+
finally {
|
|
4528
|
+
if (typeof prevTeamWorker === "string")
|
|
4529
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4530
|
+
else
|
|
4531
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
4532
|
+
if (typeof prevInternalTeamWorker === "string")
|
|
4533
|
+
process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
4534
|
+
else
|
|
4535
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4536
|
+
if (typeof prevTeamStateRoot === "string")
|
|
4537
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4538
|
+
else
|
|
4539
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4540
|
+
if (typeof prevPath === "string")
|
|
4541
|
+
process.env.PATH = prevPath;
|
|
4542
|
+
else
|
|
4543
|
+
delete process.env.PATH;
|
|
4544
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4545
|
+
}
|
|
4546
|
+
});
|
|
4547
|
+
it("blocks worker Stop when canonical task ownership has a newer non-terminal task", async () => {
|
|
4548
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-owned-task-"));
|
|
4549
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4550
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4551
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
4552
|
+
const prevPath = process.env.PATH;
|
|
4553
|
+
try {
|
|
4554
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4555
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
4556
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
4557
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
4558
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
4559
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
4560
|
+
const workerDir = join(stateDir, "team", "worker-owned-task", "workers", "worker-1");
|
|
4561
|
+
await writeJson(join(stateDir, "team", "worker-owned-task", "config.json"), {
|
|
4562
|
+
name: "worker-owned-task",
|
|
4563
|
+
tmux_session: "omx-team-worker-stop",
|
|
4564
|
+
leader_pane_id: "%42",
|
|
4565
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
4566
|
+
});
|
|
4567
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
4568
|
+
name: "worker-1",
|
|
4569
|
+
assigned_tasks: ["1"],
|
|
4570
|
+
team_state_root: stateDir,
|
|
4571
|
+
});
|
|
4572
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
4573
|
+
state: "done",
|
|
4574
|
+
current_task_id: "1",
|
|
4575
|
+
updated_at: new Date().toISOString(),
|
|
4576
|
+
});
|
|
4577
|
+
await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-1.json"), {
|
|
4578
|
+
id: "1",
|
|
4579
|
+
status: "completed",
|
|
4580
|
+
owner: "worker-1",
|
|
4581
|
+
});
|
|
4582
|
+
await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-2.json"), {
|
|
4583
|
+
id: "2",
|
|
4584
|
+
status: "in_progress",
|
|
4585
|
+
owner: "worker-1",
|
|
4586
|
+
});
|
|
4587
|
+
process.env.OMX_TEAM_WORKER = "worker-owned-task/worker-1";
|
|
4588
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4589
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4590
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
4591
|
+
const result = await dispatchCodexNativeHook({
|
|
4592
|
+
hook_event_name: "Stop",
|
|
4593
|
+
cwd,
|
|
4594
|
+
session_id: "sess-stop-team-worker-owned-task",
|
|
4595
|
+
thread_id: "thread-stop-team-worker-owned-task",
|
|
4596
|
+
turn_id: "turn-stop-team-worker-owned-task",
|
|
4597
|
+
}, { cwd });
|
|
4598
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
4599
|
+
assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_2_in_progress");
|
|
4600
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
|
|
4601
|
+
const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
|
|
4602
|
+
assert.doesNotMatch(tmuxLog, /native Stop allowed/);
|
|
3310
4603
|
}
|
|
3311
4604
|
finally {
|
|
3312
4605
|
if (typeof prevTeamWorker === "string")
|
|
3313
4606
|
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
3314
4607
|
else
|
|
3315
4608
|
delete process.env.OMX_TEAM_WORKER;
|
|
4609
|
+
if (typeof prevInternalTeamWorker === "string")
|
|
4610
|
+
process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
4611
|
+
else
|
|
4612
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
3316
4613
|
if (typeof prevTeamStateRoot === "string")
|
|
3317
4614
|
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
3318
4615
|
else
|
|
3319
4616
|
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
4617
|
+
if (typeof prevPath === "string")
|
|
4618
|
+
process.env.PATH = prevPath;
|
|
4619
|
+
else
|
|
4620
|
+
delete process.env.PATH;
|
|
3320
4621
|
await rm(cwd, { recursive: true, force: true });
|
|
3321
4622
|
}
|
|
3322
4623
|
});
|
|
@@ -3626,12 +4927,12 @@ esac
|
|
|
3626
4927
|
session_id: "sess-stop-skill",
|
|
3627
4928
|
}, { cwd });
|
|
3628
4929
|
assert.equal(result.omxEventName, "stop");
|
|
3629
|
-
assert.
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
4930
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
4931
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
4932
|
+
assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
|
|
4933
|
+
assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
4934
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
4935
|
+
assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
|
|
3635
4936
|
}
|
|
3636
4937
|
finally {
|
|
3637
4938
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -3667,7 +4968,91 @@ esac
|
|
|
3667
4968
|
await rm(cwd, { recursive: true, force: true });
|
|
3668
4969
|
}
|
|
3669
4970
|
});
|
|
3670
|
-
it("does not block on
|
|
4971
|
+
it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
|
|
4972
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
|
|
4973
|
+
try {
|
|
4974
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4975
|
+
const sessionId = "sess-stop-terminal-ralplan";
|
|
4976
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
4977
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
4978
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
4979
|
+
active: true,
|
|
4980
|
+
skill: "ralplan",
|
|
4981
|
+
phase: "planning",
|
|
4982
|
+
session_id: sessionId,
|
|
4983
|
+
active_skills: [{
|
|
4984
|
+
skill: "ralplan",
|
|
4985
|
+
phase: "planning",
|
|
4986
|
+
active: true,
|
|
4987
|
+
session_id: sessionId,
|
|
4988
|
+
}],
|
|
4989
|
+
});
|
|
4990
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
4991
|
+
active: true,
|
|
4992
|
+
mode: "ralplan",
|
|
4993
|
+
current_phase: "planning",
|
|
4994
|
+
session_id: sessionId,
|
|
4995
|
+
});
|
|
4996
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
4997
|
+
version: 1,
|
|
4998
|
+
mode: "ralplan",
|
|
4999
|
+
active: false,
|
|
5000
|
+
outcome: "finish",
|
|
5001
|
+
lifecycle_outcome: "finished",
|
|
5002
|
+
current_phase: "complete",
|
|
5003
|
+
completed_at: "2026-05-01T00:00:00.000Z",
|
|
5004
|
+
updated_at: "2026-05-01T00:00:00.000Z",
|
|
5005
|
+
});
|
|
5006
|
+
const result = await dispatchCodexNativeHook({
|
|
5007
|
+
hook_event_name: "Stop",
|
|
5008
|
+
cwd,
|
|
5009
|
+
session_id: sessionId,
|
|
5010
|
+
}, { cwd });
|
|
5011
|
+
assert.equal(result.omxEventName, "stop");
|
|
5012
|
+
assert.equal(result.outputJson, null);
|
|
5013
|
+
}
|
|
5014
|
+
finally {
|
|
5015
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5016
|
+
}
|
|
5017
|
+
});
|
|
5018
|
+
it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
|
|
5019
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
|
|
5020
|
+
try {
|
|
5021
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5022
|
+
const sessionId = "sess-stop-current-ralplan";
|
|
5023
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5024
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
5025
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
5026
|
+
active: true,
|
|
5027
|
+
skill: "ralplan",
|
|
5028
|
+
phase: "planning",
|
|
5029
|
+
session_id: sessionId,
|
|
5030
|
+
active_skills: [{
|
|
5031
|
+
skill: "ralplan",
|
|
5032
|
+
phase: "planning",
|
|
5033
|
+
active: true,
|
|
5034
|
+
session_id: sessionId,
|
|
5035
|
+
}],
|
|
5036
|
+
});
|
|
5037
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
5038
|
+
active: true,
|
|
5039
|
+
mode: "ralplan",
|
|
5040
|
+
current_phase: "planning",
|
|
5041
|
+
session_id: "sess-other-ralplan",
|
|
5042
|
+
});
|
|
5043
|
+
const result = await dispatchCodexNativeHook({
|
|
5044
|
+
hook_event_name: "Stop",
|
|
5045
|
+
cwd,
|
|
5046
|
+
session_id: sessionId,
|
|
5047
|
+
}, { cwd });
|
|
5048
|
+
assert.equal(result.omxEventName, "stop");
|
|
5049
|
+
assert.equal(result.outputJson, null);
|
|
5050
|
+
}
|
|
5051
|
+
finally {
|
|
5052
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5053
|
+
}
|
|
5054
|
+
});
|
|
5055
|
+
it("returns an explicit ralplan waiting status while subagents are still active", async () => {
|
|
3671
5056
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
|
|
3672
5057
|
try {
|
|
3673
5058
|
const stateDir = join(cwd, ".omx", "state");
|
|
@@ -3714,7 +5099,11 @@ esac
|
|
|
3714
5099
|
session_id: "sess-stop-skill-subagent",
|
|
3715
5100
|
}, { cwd });
|
|
3716
5101
|
assert.equal(result.omxEventName, "stop");
|
|
3717
|
-
assert.equal(result.outputJson,
|
|
5102
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
5103
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Status: waiting/);
|
|
5104
|
+
assert.match(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
|
|
5105
|
+
assert.match(String(result.outputJson?.reason ?? ""), /then continue from the current ralplan artifact/i);
|
|
5106
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_waiting_subagent");
|
|
3718
5107
|
}
|
|
3719
5108
|
finally {
|
|
3720
5109
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -3833,6 +5222,31 @@ esac
|
|
|
3833
5222
|
await rm(cwd, { recursive: true, force: true });
|
|
3834
5223
|
}
|
|
3835
5224
|
});
|
|
5225
|
+
it("does not block Stop from stale root autoresearch state when the explicit session directory is missing", async () => {
|
|
5226
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-missing-session-autoresearch-"));
|
|
5227
|
+
try {
|
|
5228
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5229
|
+
await mkdir(stateDir, { recursive: true });
|
|
5230
|
+
await writeJson(join(stateDir, "autoresearch-state.json"), {
|
|
5231
|
+
active: true,
|
|
5232
|
+
mode: "autoresearch",
|
|
5233
|
+
current_phase: "executing",
|
|
5234
|
+
validation_mode: "mission-validator-script",
|
|
5235
|
+
mission_validator_command: "node scripts/validate.js",
|
|
5236
|
+
completion_artifact_path: ".omx/specs/autoresearch-demo/completion.json",
|
|
5237
|
+
});
|
|
5238
|
+
const result = await dispatchCodexNativeHook({
|
|
5239
|
+
hook_event_name: "Stop",
|
|
5240
|
+
cwd,
|
|
5241
|
+
session_id: "missing-session",
|
|
5242
|
+
}, { cwd });
|
|
5243
|
+
assert.equal(result.omxEventName, "stop");
|
|
5244
|
+
assert.equal(result.outputJson, null);
|
|
5245
|
+
}
|
|
5246
|
+
finally {
|
|
5247
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5248
|
+
}
|
|
5249
|
+
});
|
|
3836
5250
|
it("does not block Stop solely because deep-interview is active", async () => {
|
|
3837
5251
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
|
|
3838
5252
|
try {
|
|
@@ -4527,6 +5941,205 @@ esac
|
|
|
4527
5941
|
await rm(cwd, { recursive: true, force: true });
|
|
4528
5942
|
}
|
|
4529
5943
|
});
|
|
5944
|
+
it("does not block a question-only pane from Ralph state owned by another Codex session", async () => {
|
|
5945
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-question-pane-"));
|
|
5946
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
5947
|
+
try {
|
|
5948
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5949
|
+
const questionSessionId = "sess-question-pane";
|
|
5950
|
+
const questionNativeSessionId = "codex-question-pane";
|
|
5951
|
+
await mkdir(join(stateDir, "sessions", questionSessionId), { recursive: true });
|
|
5952
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
5953
|
+
session_id: questionSessionId,
|
|
5954
|
+
native_session_id: questionNativeSessionId,
|
|
5955
|
+
cwd,
|
|
5956
|
+
});
|
|
5957
|
+
await writeJson(join(stateDir, "sessions", questionSessionId, "ralph-state.json"), {
|
|
5958
|
+
active: true,
|
|
5959
|
+
mode: "ralph",
|
|
5960
|
+
current_phase: "executing",
|
|
5961
|
+
session_id: questionSessionId,
|
|
5962
|
+
owner_omx_session_id: "sess-ralph-owner",
|
|
5963
|
+
owner_codex_session_id: "codex-ralph-owner",
|
|
5964
|
+
thread_id: "thread-ralph-owner",
|
|
5965
|
+
tmux_pane_id: "%41",
|
|
5966
|
+
});
|
|
5967
|
+
process.env.TMUX_PANE = "%99";
|
|
5968
|
+
const result = await dispatchCodexNativeHook({
|
|
5969
|
+
hook_event_name: "Stop",
|
|
5970
|
+
cwd,
|
|
5971
|
+
session_id: questionNativeSessionId,
|
|
5972
|
+
thread_id: "thread-question-pane",
|
|
5973
|
+
}, { cwd });
|
|
5974
|
+
assert.equal(result.omxEventName, "stop");
|
|
5975
|
+
assert.equal(result.outputJson, null);
|
|
5976
|
+
}
|
|
5977
|
+
finally {
|
|
5978
|
+
if (typeof previousTmuxPane === "string")
|
|
5979
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
5980
|
+
else
|
|
5981
|
+
delete process.env.TMUX_PANE;
|
|
5982
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5983
|
+
}
|
|
5984
|
+
});
|
|
5985
|
+
it("does not block Stop when Ralph skill-active initialization points at another session", async () => {
|
|
5986
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-stale-skill-active-"));
|
|
5987
|
+
try {
|
|
5988
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5989
|
+
const currentSessionId = "sess-current-ralph";
|
|
5990
|
+
await mkdir(join(stateDir, "sessions", currentSessionId), { recursive: true });
|
|
5991
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
5992
|
+
session_id: currentSessionId,
|
|
5993
|
+
native_session_id: currentSessionId,
|
|
5994
|
+
cwd,
|
|
5995
|
+
});
|
|
5996
|
+
await writeJson(join(stateDir, "sessions", currentSessionId, "ralph-state.json"), {
|
|
5997
|
+
active: true,
|
|
5998
|
+
mode: "ralph",
|
|
5999
|
+
current_phase: "verifying",
|
|
6000
|
+
session_id: currentSessionId,
|
|
6001
|
+
owner_omx_session_id: currentSessionId,
|
|
6002
|
+
task_slug: "stale-rebound-task",
|
|
6003
|
+
});
|
|
6004
|
+
await writeJson(join(stateDir, "sessions", currentSessionId, "skill-active-state.json"), {
|
|
6005
|
+
active: true,
|
|
6006
|
+
skill: "ralph",
|
|
6007
|
+
phase: "verifying",
|
|
6008
|
+
session_id: currentSessionId,
|
|
6009
|
+
initialized_mode: "ralph",
|
|
6010
|
+
initialized_state_path: ".omx/state/sessions/sess-old-ralph/ralph-state.json",
|
|
6011
|
+
active_skills: [{ skill: "ralph", phase: "verifying", active: true, session_id: currentSessionId }],
|
|
6012
|
+
});
|
|
6013
|
+
const result = await dispatchCodexNativeHook({
|
|
6014
|
+
hook_event_name: "Stop",
|
|
6015
|
+
cwd,
|
|
6016
|
+
session_id: currentSessionId,
|
|
6017
|
+
}, { cwd });
|
|
6018
|
+
assert.equal(result.omxEventName, "stop");
|
|
6019
|
+
assert.equal(result.outputJson, null);
|
|
6020
|
+
}
|
|
6021
|
+
finally {
|
|
6022
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6023
|
+
}
|
|
6024
|
+
});
|
|
6025
|
+
it("blocks same-session Ralph Stop continuation when ownership identifiers match", async () => {
|
|
6026
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-owned-session-"));
|
|
6027
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
6028
|
+
try {
|
|
6029
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6030
|
+
const omxSessionId = "sess-ralph-owned";
|
|
6031
|
+
const nativeSessionId = "codex-ralph-owned";
|
|
6032
|
+
await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
|
|
6033
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
6034
|
+
session_id: omxSessionId,
|
|
6035
|
+
native_session_id: nativeSessionId,
|
|
6036
|
+
cwd,
|
|
6037
|
+
});
|
|
6038
|
+
await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
|
|
6039
|
+
active: true,
|
|
6040
|
+
mode: "ralph",
|
|
6041
|
+
current_phase: "executing",
|
|
6042
|
+
session_id: omxSessionId,
|
|
6043
|
+
owner_omx_session_id: omxSessionId,
|
|
6044
|
+
owner_codex_session_id: nativeSessionId,
|
|
6045
|
+
thread_id: "thread-ralph-owned",
|
|
6046
|
+
tmux_pane_id: "%42",
|
|
6047
|
+
});
|
|
6048
|
+
process.env.TMUX_PANE = "%42";
|
|
6049
|
+
const result = await dispatchCodexNativeHook({
|
|
6050
|
+
hook_event_name: "Stop",
|
|
6051
|
+
cwd,
|
|
6052
|
+
session_id: nativeSessionId,
|
|
6053
|
+
thread_id: "thread-ralph-owned",
|
|
6054
|
+
}, { cwd });
|
|
6055
|
+
assert.equal(result.omxEventName, "stop");
|
|
6056
|
+
assert.deepEqual(result.outputJson, {
|
|
6057
|
+
decision: "block",
|
|
6058
|
+
reason: "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-ralph-owned/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
6059
|
+
stopReason: "ralph_executing",
|
|
6060
|
+
systemMessage: "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-ralph-owned/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
6063
|
+
finally {
|
|
6064
|
+
if (typeof previousTmuxPane === "string")
|
|
6065
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
6066
|
+
else
|
|
6067
|
+
delete process.env.TMUX_PANE;
|
|
6068
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6069
|
+
}
|
|
6070
|
+
});
|
|
6071
|
+
it("allows native verifier subagent Stop to complete while leader Ralph remains active", async () => {
|
|
6072
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-subagent-verdict-"));
|
|
6073
|
+
try {
|
|
6074
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6075
|
+
const omxSessionId = "sess-ralph-leader-verifier";
|
|
6076
|
+
const leaderNativeSessionId = "codex-ralph-leader-verifier";
|
|
6077
|
+
const childNativeSessionId = "codex-verifier-child";
|
|
6078
|
+
await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
|
|
6079
|
+
await writeSessionStart(cwd, omxSessionId, {
|
|
6080
|
+
nativeSessionId: leaderNativeSessionId,
|
|
6081
|
+
});
|
|
6082
|
+
await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
|
|
6083
|
+
active: true,
|
|
6084
|
+
mode: "ralph",
|
|
6085
|
+
current_phase: "verifying",
|
|
6086
|
+
session_id: omxSessionId,
|
|
6087
|
+
owner_omx_session_id: omxSessionId,
|
|
6088
|
+
owner_codex_session_id: leaderNativeSessionId,
|
|
6089
|
+
});
|
|
6090
|
+
const transcriptPath = join(cwd, "verifier-subagent-rollout.jsonl");
|
|
6091
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
6092
|
+
type: "session_meta",
|
|
6093
|
+
payload: {
|
|
6094
|
+
id: childNativeSessionId,
|
|
6095
|
+
source: {
|
|
6096
|
+
subagent: {
|
|
6097
|
+
thread_spawn: {
|
|
6098
|
+
parent_thread_id: leaderNativeSessionId,
|
|
6099
|
+
depth: 1,
|
|
6100
|
+
agent_nickname: "Verifier",
|
|
6101
|
+
agent_role: "verifier",
|
|
6102
|
+
},
|
|
6103
|
+
},
|
|
6104
|
+
},
|
|
6105
|
+
agent_nickname: "Verifier",
|
|
6106
|
+
agent_role: "verifier",
|
|
6107
|
+
},
|
|
6108
|
+
})}\n`);
|
|
6109
|
+
await dispatchCodexNativeHook({
|
|
6110
|
+
hook_event_name: "SessionStart",
|
|
6111
|
+
cwd,
|
|
6112
|
+
session_id: childNativeSessionId,
|
|
6113
|
+
transcript_path: transcriptPath,
|
|
6114
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
6115
|
+
const childStop = await dispatchCodexNativeHook({
|
|
6116
|
+
hook_event_name: "Stop",
|
|
6117
|
+
cwd,
|
|
6118
|
+
session_id: childNativeSessionId,
|
|
6119
|
+
thread_id: childNativeSessionId,
|
|
6120
|
+
last_assistant_message: "Verdict: APPROVED. Evidence is sufficient.",
|
|
6121
|
+
}, { cwd });
|
|
6122
|
+
assert.equal(childStop.omxEventName, "stop");
|
|
6123
|
+
assert.equal(childStop.outputJson, null);
|
|
6124
|
+
const leaderStop = await dispatchCodexNativeHook({
|
|
6125
|
+
hook_event_name: "Stop",
|
|
6126
|
+
cwd,
|
|
6127
|
+
session_id: leaderNativeSessionId,
|
|
6128
|
+
thread_id: leaderNativeSessionId,
|
|
6129
|
+
last_assistant_message: "Waiting on verification integration.",
|
|
6130
|
+
}, { cwd });
|
|
6131
|
+
assert.equal(leaderStop.omxEventName, "stop");
|
|
6132
|
+
assert.deepEqual(leaderStop.outputJson, {
|
|
6133
|
+
decision: "block",
|
|
6134
|
+
reason: "OMX Ralph is still active (phase: verifying; state: .omx/state/sessions/sess-ralph-leader-verifier/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
6135
|
+
stopReason: "ralph_verifying",
|
|
6136
|
+
systemMessage: "OMX Ralph is still active (phase: verifying; state: .omx/state/sessions/sess-ralph-leader-verifier/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
6137
|
+
});
|
|
6138
|
+
}
|
|
6139
|
+
finally {
|
|
6140
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6141
|
+
}
|
|
6142
|
+
});
|
|
4530
6143
|
it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
|
|
4531
6144
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
|
|
4532
6145
|
try {
|
|
@@ -5062,12 +6675,11 @@ esac
|
|
|
5062
6675
|
await rm(cwd, { recursive: true, force: true });
|
|
5063
6676
|
}
|
|
5064
6677
|
});
|
|
5065
|
-
it("suppresses native auto-nudge when root deep-interview mode state is active
|
|
6678
|
+
it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
|
|
5066
6679
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
|
|
5067
6680
|
try {
|
|
5068
6681
|
const stateDir = join(cwd, ".omx", "state");
|
|
5069
6682
|
await mkdir(stateDir, { recursive: true });
|
|
5070
|
-
process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
|
|
5071
6683
|
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
5072
6684
|
active: true,
|
|
5073
6685
|
mode: "deep-interview",
|
|
@@ -5086,6 +6698,57 @@ esac
|
|
|
5086
6698
|
await rm(cwd, { recursive: true, force: true });
|
|
5087
6699
|
}
|
|
5088
6700
|
});
|
|
6701
|
+
it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
|
|
6702
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
|
|
6703
|
+
try {
|
|
6704
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6705
|
+
await mkdir(stateDir, { recursive: true });
|
|
6706
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
|
|
6707
|
+
const result = await dispatchCodexNativeHook({
|
|
6708
|
+
hook_event_name: "Stop",
|
|
6709
|
+
cwd,
|
|
6710
|
+
thread_id: "thread-stop-auto-env-session",
|
|
6711
|
+
turn_id: "turn-stop-auto-env-session-1",
|
|
6712
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
6713
|
+
}, { cwd });
|
|
6714
|
+
assert.equal(result.omxEventName, "stop");
|
|
6715
|
+
assert.deepEqual(result.outputJson, {
|
|
6716
|
+
decision: "block",
|
|
6717
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
6718
|
+
stopReason: "auto_nudge",
|
|
6719
|
+
systemMessage: "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
6720
|
+
});
|
|
6721
|
+
const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
|
|
6722
|
+
assert.ok(stopState.sessions["sess-stop-auto-mode"]);
|
|
6723
|
+
}
|
|
6724
|
+
finally {
|
|
6725
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6726
|
+
}
|
|
6727
|
+
});
|
|
6728
|
+
it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
|
|
6729
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
|
|
6730
|
+
try {
|
|
6731
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6732
|
+
await mkdir(stateDir, { recursive: true });
|
|
6733
|
+
process.env.SESSION_ID = "generic-shell-session";
|
|
6734
|
+
const result = await dispatchCodexNativeHook({
|
|
6735
|
+
hook_event_name: "Stop",
|
|
6736
|
+
cwd,
|
|
6737
|
+
thread_id: "thread-stop-auto-generic-session",
|
|
6738
|
+
turn_id: "turn-stop-auto-generic-session-1",
|
|
6739
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
6740
|
+
}, { cwd });
|
|
6741
|
+
assert.equal(result.omxEventName, "stop");
|
|
6742
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6743
|
+
const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
|
|
6744
|
+
const sessions = stopState.sessions;
|
|
6745
|
+
assert.equal(sessions["generic-shell-session"], undefined);
|
|
6746
|
+
assert.ok(sessions["thread-stop-auto-generic-session"]);
|
|
6747
|
+
}
|
|
6748
|
+
finally {
|
|
6749
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6750
|
+
}
|
|
6751
|
+
});
|
|
5089
6752
|
it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
|
|
5090
6753
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
|
|
5091
6754
|
try {
|
|
@@ -5368,8 +7031,8 @@ esac
|
|
|
5368
7031
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
|
|
5369
7032
|
try {
|
|
5370
7033
|
const stateDir = join(cwd, ".omx", "state");
|
|
5371
|
-
await mkdir(stateDir, { recursive: true });
|
|
5372
|
-
await writeJson(join(stateDir, "ultrawork-state.json"), {
|
|
7034
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
|
|
7035
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
|
|
5373
7036
|
active: true,
|
|
5374
7037
|
current_phase: "executing",
|
|
5375
7038
|
});
|
|
@@ -5441,12 +7104,10 @@ esac
|
|
|
5441
7104
|
stop_hook_active: true,
|
|
5442
7105
|
}, { cwd });
|
|
5443
7106
|
assert.equal(repeated.omxEventName, "stop");
|
|
5444
|
-
assert.
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
systemMessage: "OMX skill ralplan is still active (phase: planning).",
|
|
5449
|
-
});
|
|
7107
|
+
assert.equal(repeated.outputJson?.decision, "block");
|
|
7108
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
7109
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
7110
|
+
assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
5450
7111
|
}
|
|
5451
7112
|
finally {
|
|
5452
7113
|
await rm(cwd, { recursive: true, force: true });
|