oh-my-codex 0.15.3 → 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/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 +20 -1
- 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-warning-copy.test.js +23 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +8 -1
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +82 -3
- 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__/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__/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 +18 -9
- 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 +187 -0
- 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 +49 -0
- 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 +6 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +145 -18
- package/dist/cli/index.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.map +1 -1
- package/dist/cli/ralph.js +8 -0
- 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 +2 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +72 -17
- 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 +12 -1
- 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 +3 -2
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +52 -8
- 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__/anti-slop-workflow.test.js +3 -3
- 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 +2 -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__/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 +62 -1
- 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 +30 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
- package/dist/pipeline/stages/team-exec.js +2 -19
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/planning/__tests__/artifacts.test.js +16 -1
- package/dist/planning/__tests__/artifacts.test.js.map +1 -1
- package/dist/planning/artifacts.d.ts +1 -0
- package/dist/planning/artifacts.d.ts.map +1 -1
- package/dist/planning/artifacts.js +9 -12
- package/dist/planning/artifacts.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 +1516 -205
- 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 +497 -51
- 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 +222 -19
- 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-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/run-test-files.js +17 -1
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +2 -2
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/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 +35 -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 +59 -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__/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.test.js +118 -6
- 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__/tmux-session.test.js +3 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +50 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +4 -3
- 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.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/role-router.js +5 -5
- package/dist/team/role-router.js.map +1 -1
- package/dist/team/runtime.d.ts +6 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +46 -6
- 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/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +4 -2
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +2 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +19 -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/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/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 +1654 -157
- 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 +592 -52
- package/src/scripts/codex-native-pre-post.ts +252 -20
- 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-stop.ts +246 -0
- package/src/scripts/notify-hook/team-worker.ts +23 -14
- package/src/scripts/run-test-files.ts +20 -1
- package/src/scripts/sync-plugin-mirror.ts +2 -2
- 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
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { writeSessionStart } from "../../hooks/session.js";
|
|
24
24
|
import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
25
25
|
import { executeStateOperation } from "../../state/operations.js";
|
|
26
|
+
import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
|
|
26
27
|
|
|
27
28
|
function nativeHookScriptPath(): string {
|
|
28
29
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
@@ -57,6 +58,52 @@ async function writeJson(path: string, value: unknown): Promise<void> {
|
|
|
57
58
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
function buildWorkerStopFakeTmux(tmuxLogPath: string, options: { failSend?: boolean } = {}): string {
|
|
62
|
+
return `#!/usr/bin/env bash
|
|
63
|
+
set -eu
|
|
64
|
+
echo "$@" >> "${tmuxLogPath}"
|
|
65
|
+
cmd="$1"
|
|
66
|
+
shift || true
|
|
67
|
+
if [[ "$cmd" == "display-message" ]]; then
|
|
68
|
+
fmt=""
|
|
69
|
+
while [[ "$#" -gt 0 ]]; do
|
|
70
|
+
case "$1" in
|
|
71
|
+
-p) ;;
|
|
72
|
+
-t) shift ;;
|
|
73
|
+
*) fmt="$1" ;;
|
|
74
|
+
esac
|
|
75
|
+
shift || true
|
|
76
|
+
done
|
|
77
|
+
case "$fmt" in
|
|
78
|
+
"#{pane_in_mode}") echo "0" ;;
|
|
79
|
+
"#{pane_id}") echo "%42" ;;
|
|
80
|
+
"#{pane_current_path}") pwd ;;
|
|
81
|
+
"#{pane_start_command}") echo "codex" ;;
|
|
82
|
+
"#{pane_current_command}") echo "codex" ;;
|
|
83
|
+
"#S") echo "omx-team-worker-stop" ;;
|
|
84
|
+
*) ;;
|
|
85
|
+
esac
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
if [[ "$cmd" == "capture-pane" ]]; then
|
|
89
|
+
echo "› ready"
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
if [[ "$cmd" == "send-keys" ]]; then
|
|
93
|
+
${options.failSend ? "exit 1" : "exit 0"}
|
|
94
|
+
fi
|
|
95
|
+
exit 0
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function initTempGitRepo(prefix: string): Promise<string> {
|
|
100
|
+
const cwd = await mkdtemp(join(tmpdir(), prefix));
|
|
101
|
+
execFileSync("git", ["init"], { cwd, stdio: "ignore" });
|
|
102
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
|
|
103
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
|
|
104
|
+
return cwd;
|
|
105
|
+
}
|
|
106
|
+
|
|
60
107
|
async function writeActiveAutopilotSession(cwd: string, sessionId: string): Promise<void> {
|
|
61
108
|
await writeJson(join(cwd, ".omx", "state", "session.json"), {
|
|
62
109
|
session_id: sessionId,
|
|
@@ -141,10 +188,12 @@ const TEAM_ENV_KEYS = [
|
|
|
141
188
|
"OMX_TEAM_STATE_ROOT",
|
|
142
189
|
"OMX_TEAM_LEADER_CWD",
|
|
143
190
|
"OMX_SESSION_ID",
|
|
191
|
+
"SESSION_ID",
|
|
144
192
|
"OMX_QUESTION_RETURN_PANE",
|
|
145
193
|
"OMX_LEADER_PANE_ID",
|
|
146
194
|
"TMUX",
|
|
147
195
|
"TMUX_PANE",
|
|
196
|
+
"OMX_TMUX_HUD_OWNER",
|
|
148
197
|
] as const;
|
|
149
198
|
|
|
150
199
|
const priorTeamEnv = new Map<(typeof TEAM_ENV_KEYS)[number], string | undefined>();
|
|
@@ -999,6 +1048,124 @@ describe("codex native hook dispatch", () => {
|
|
|
999
1048
|
}
|
|
1000
1049
|
});
|
|
1001
1050
|
|
|
1051
|
+
it("warns completion-like prompts when active goal workflows need Codex snapshot reconciliation", async () => {
|
|
1052
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-warning-"));
|
|
1053
|
+
try {
|
|
1054
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
1055
|
+
version: 1,
|
|
1056
|
+
activeGoalId: "G001-demo",
|
|
1057
|
+
goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
const result = await dispatchCodexNativeHook({
|
|
1061
|
+
hook_event_name: "UserPromptSubmit",
|
|
1062
|
+
cwd,
|
|
1063
|
+
session_id: "sess-goal-warning",
|
|
1064
|
+
thread_id: "thread-goal-warning",
|
|
1065
|
+
prompt: "complete this goal now",
|
|
1066
|
+
}, { cwd });
|
|
1067
|
+
|
|
1068
|
+
assert.match(JSON.stringify(result.outputJson), /requires Codex goal snapshot reconciliation/);
|
|
1069
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal/);
|
|
1070
|
+
assert.match(JSON.stringify(result.outputJson), /--codex-goal-json/);
|
|
1071
|
+
} finally {
|
|
1072
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it("blocks Stop when a completion-like final answer skips active goal snapshot reconciliation", async () => {
|
|
1077
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-stop-"));
|
|
1078
|
+
try {
|
|
1079
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
1080
|
+
version: 1,
|
|
1081
|
+
workflow: "performance-goal",
|
|
1082
|
+
slug: "latency",
|
|
1083
|
+
objective: "Reduce latency",
|
|
1084
|
+
status: "validation_passed",
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
const result = await dispatchCodexNativeHook({
|
|
1088
|
+
hook_event_name: "Stop",
|
|
1089
|
+
cwd,
|
|
1090
|
+
session_id: "sess-goal-stop",
|
|
1091
|
+
thread_id: "thread-goal-stop",
|
|
1092
|
+
last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
|
|
1093
|
+
}, { cwd });
|
|
1094
|
+
|
|
1095
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
1096
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1097
|
+
assert.match(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
|
|
1098
|
+
assert.match(JSON.stringify(result.outputJson), /Hooks must not mutate Codex goal state/);
|
|
1099
|
+
} finally {
|
|
1100
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
|
|
1105
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
|
|
1106
|
+
try {
|
|
1107
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1108
|
+
const canonicalSessionId = "sess-parent";
|
|
1109
|
+
const leaderNativeSessionId = "native-parent-thread";
|
|
1110
|
+
const childNativeSessionId = "native-child-thread";
|
|
1111
|
+
const nowIso = new Date().toISOString();
|
|
1112
|
+
|
|
1113
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
1114
|
+
session_id: canonicalSessionId,
|
|
1115
|
+
native_session_id: leaderNativeSessionId,
|
|
1116
|
+
});
|
|
1117
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
1118
|
+
schemaVersion: 1,
|
|
1119
|
+
sessions: {
|
|
1120
|
+
[canonicalSessionId]: {
|
|
1121
|
+
session_id: canonicalSessionId,
|
|
1122
|
+
leader_thread_id: leaderNativeSessionId,
|
|
1123
|
+
updated_at: nowIso,
|
|
1124
|
+
threads: {
|
|
1125
|
+
[leaderNativeSessionId]: {
|
|
1126
|
+
thread_id: leaderNativeSessionId,
|
|
1127
|
+
kind: "leader",
|
|
1128
|
+
first_seen_at: nowIso,
|
|
1129
|
+
last_seen_at: nowIso,
|
|
1130
|
+
turn_count: 1,
|
|
1131
|
+
},
|
|
1132
|
+
[childNativeSessionId]: {
|
|
1133
|
+
thread_id: childNativeSessionId,
|
|
1134
|
+
kind: "subagent",
|
|
1135
|
+
first_seen_at: nowIso,
|
|
1136
|
+
last_seen_at: nowIso,
|
|
1137
|
+
turn_count: 1,
|
|
1138
|
+
mode: "architect",
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const result = await dispatchCodexNativeHook(
|
|
1146
|
+
{
|
|
1147
|
+
hook_event_name: "UserPromptSubmit",
|
|
1148
|
+
cwd,
|
|
1149
|
+
session_id: childNativeSessionId,
|
|
1150
|
+
thread_id: childNativeSessionId,
|
|
1151
|
+
turn_id: "turn-child-1",
|
|
1152
|
+
prompt: "$ralplan Architect review step. Review the draft plan and return APPROVE or ITERATE.",
|
|
1153
|
+
},
|
|
1154
|
+
{ cwd },
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1158
|
+
assert.equal(result.skillState, null);
|
|
1159
|
+
assert.equal(result.outputJson, null);
|
|
1160
|
+
assert.equal(existsSync(join(stateDir, "skill-active-state.json")), false);
|
|
1161
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), false);
|
|
1162
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
|
|
1163
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
|
|
1164
|
+
} finally {
|
|
1165
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1002
1169
|
it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
|
|
1003
1170
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
|
|
1004
1171
|
try {
|
|
@@ -1028,6 +1195,39 @@ describe("codex native hook dispatch", () => {
|
|
|
1028
1195
|
}
|
|
1029
1196
|
});
|
|
1030
1197
|
|
|
1198
|
+
it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
|
|
1199
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
|
|
1200
|
+
try {
|
|
1201
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1202
|
+
const result = await dispatchCodexNativeHook(
|
|
1203
|
+
{
|
|
1204
|
+
hook_event_name: "UserPromptSubmit",
|
|
1205
|
+
cwd,
|
|
1206
|
+
session_id: "sess-ultragoal-1",
|
|
1207
|
+
thread_id: "thread-ultragoal-1",
|
|
1208
|
+
turn_id: "turn-ultragoal-1",
|
|
1209
|
+
prompt: "$ultragoal split this launch into durable goals",
|
|
1210
|
+
},
|
|
1211
|
+
{ cwd },
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1215
|
+
assert.equal(result.skillState?.skill, "ultragoal");
|
|
1216
|
+
assert.equal(result.skillState?.initialized_mode, undefined);
|
|
1217
|
+
const message = String(
|
|
1218
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
1219
|
+
);
|
|
1220
|
+
assert.match(message, /"\$ultragoal" -> ultragoal/);
|
|
1221
|
+
assert.match(message, /Ultragoal protocol:/);
|
|
1222
|
+
assert.match(message, /get_goal/);
|
|
1223
|
+
assert.match(message, /create_goal/);
|
|
1224
|
+
assert.match(message, /update_goal/);
|
|
1225
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
|
|
1226
|
+
} finally {
|
|
1227
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1031
1231
|
it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
|
|
1032
1232
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
|
|
1033
1233
|
try {
|
|
@@ -1362,7 +1562,6 @@ describe("codex native hook dispatch", () => {
|
|
|
1362
1562
|
}
|
|
1363
1563
|
});
|
|
1364
1564
|
|
|
1365
|
-
|
|
1366
1565
|
it("includes leader-pane preservation guidance when a pane hint is available", async () => {
|
|
1367
1566
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-pane-hint-"));
|
|
1368
1567
|
try {
|
|
@@ -1502,7 +1701,6 @@ describe("codex native hook dispatch", () => {
|
|
|
1502
1701
|
}
|
|
1503
1702
|
});
|
|
1504
1703
|
|
|
1505
|
-
|
|
1506
1704
|
it("ignores generic wrapper fields so metadata cannot trigger workflow routing or Stop blocking", async () => {
|
|
1507
1705
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-wrapper-metadata-"));
|
|
1508
1706
|
try {
|
|
@@ -1916,10 +2114,12 @@ export async function onHookEvent(event) {
|
|
|
1916
2114
|
const originalTmux = process.env.TMUX;
|
|
1917
2115
|
const originalTmuxPane = process.env.TMUX_PANE;
|
|
1918
2116
|
const originalPath = process.env.PATH;
|
|
2117
|
+
const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
1919
2118
|
const originalArgv = process.argv;
|
|
1920
2119
|
try {
|
|
1921
2120
|
process.env.TMUX = "1";
|
|
1922
2121
|
process.env.TMUX_PANE = "%1";
|
|
2122
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
|
|
1923
2123
|
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1924
2124
|
await writeFile(
|
|
1925
2125
|
join(cwd, ".omx", "hud-config.json"),
|
|
@@ -1980,12 +2180,64 @@ esac
|
|
|
1980
2180
|
} else {
|
|
1981
2181
|
process.env.TMUX_PANE = originalTmuxPane;
|
|
1982
2182
|
}
|
|
2183
|
+
if (originalHudOwner === undefined) {
|
|
2184
|
+
delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
2185
|
+
} else {
|
|
2186
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
|
|
2187
|
+
}
|
|
1983
2188
|
process.env.PATH = originalPath;
|
|
1984
2189
|
process.argv = originalArgv;
|
|
1985
2190
|
await rm(cwd, { recursive: true, force: true });
|
|
1986
2191
|
}
|
|
1987
2192
|
});
|
|
1988
2193
|
|
|
2194
|
+
it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
|
|
2195
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
|
|
2196
|
+
const originalTmux = process.env.TMUX;
|
|
2197
|
+
const originalTmuxPane = process.env.TMUX_PANE;
|
|
2198
|
+
const originalPath = process.env.PATH;
|
|
2199
|
+
const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
2200
|
+
try {
|
|
2201
|
+
process.env.TMUX = "1";
|
|
2202
|
+
process.env.TMUX_PANE = "%claude";
|
|
2203
|
+
delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
2204
|
+
|
|
2205
|
+
const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-bin-"));
|
|
2206
|
+
const tmuxLog = join(cwd, "tmux.log");
|
|
2207
|
+
await writeFile(
|
|
2208
|
+
join(binDir, "tmux"),
|
|
2209
|
+
`#!/usr/bin/env bash
|
|
2210
|
+
printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
|
|
2211
|
+
exit 0
|
|
2212
|
+
`,
|
|
2213
|
+
);
|
|
2214
|
+
await chmod(join(binDir, "tmux"), 0o755);
|
|
2215
|
+
process.env.PATH = `${binDir}:${originalPath}`;
|
|
2216
|
+
|
|
2217
|
+
const result = await dispatchCodexNativeHook(
|
|
2218
|
+
{
|
|
2219
|
+
hook_event_name: "UserPromptSubmit",
|
|
2220
|
+
cwd,
|
|
2221
|
+
session_id: "sess-hud-unowned",
|
|
2222
|
+
prompt: "$ralplan prepare plan",
|
|
2223
|
+
},
|
|
2224
|
+
{ cwd },
|
|
2225
|
+
);
|
|
2226
|
+
|
|
2227
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2228
|
+
assert.equal(existsSync(tmuxLog), false);
|
|
2229
|
+
} finally {
|
|
2230
|
+
if (originalTmux === undefined) delete process.env.TMUX;
|
|
2231
|
+
else process.env.TMUX = originalTmux;
|
|
2232
|
+
if (originalTmuxPane === undefined) delete process.env.TMUX_PANE;
|
|
2233
|
+
else process.env.TMUX_PANE = originalTmuxPane;
|
|
2234
|
+
if (originalHudOwner === undefined) delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
2235
|
+
else process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
|
|
2236
|
+
process.env.PATH = originalPath;
|
|
2237
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
|
|
1989
2241
|
it("blocks Bash omx question when no leader-pane return hint is preserved", async () => {
|
|
1990
2242
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-enforce-"));
|
|
1991
2243
|
try {
|
|
@@ -2538,124 +2790,595 @@ esac
|
|
|
2538
2790
|
}
|
|
2539
2791
|
});
|
|
2540
2792
|
|
|
2541
|
-
it("
|
|
2542
|
-
const cwd = await
|
|
2793
|
+
it("blocks Stop for untracked non-Bash-style sloppy fallback source edits", async () => {
|
|
2794
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-untracked-");
|
|
2543
2795
|
try {
|
|
2796
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2797
|
+
await writeFile(
|
|
2798
|
+
join(cwd, "src", "runtime.ts"),
|
|
2799
|
+
[
|
|
2800
|
+
"export function loadRuntime() {",
|
|
2801
|
+
" // implement a quick hack fallback if it fails",
|
|
2802
|
+
" return process.env.RUNTIME || 'local';",
|
|
2803
|
+
"}",
|
|
2804
|
+
].join("\n"),
|
|
2805
|
+
);
|
|
2806
|
+
|
|
2544
2807
|
const result = await dispatchCodexNativeHook(
|
|
2545
|
-
{
|
|
2546
|
-
hook_event_name: "PreToolUse",
|
|
2547
|
-
cwd,
|
|
2548
|
-
tool_name: "Bash",
|
|
2549
|
-
tool_use_id: "tool-slop-git-priority",
|
|
2550
|
-
tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
|
|
2551
|
-
},
|
|
2808
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-untracked" },
|
|
2552
2809
|
{ cwd },
|
|
2553
2810
|
);
|
|
2554
2811
|
|
|
2555
|
-
assert.equal(result.omxEventName, "
|
|
2812
|
+
assert.equal(result.omxEventName, "stop");
|
|
2556
2813
|
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2557
|
-
assert.
|
|
2558
|
-
assert.
|
|
2814
|
+
assert.equal((result.outputJson as { stopReason?: string } | null)?.stopReason, "sloppy_fallback_diff_audit");
|
|
2815
|
+
assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
|
|
2816
|
+
assert.match(JSON.stringify(result.outputJson), /grounded design/);
|
|
2559
2817
|
} finally {
|
|
2560
2818
|
await rm(cwd, { recursive: true, force: true });
|
|
2561
2819
|
}
|
|
2562
2820
|
});
|
|
2563
2821
|
|
|
2564
|
-
it("
|
|
2565
|
-
const cwd = await
|
|
2822
|
+
it("keeps blocking repeated Stop while sloppy fallback diff remains", async () => {
|
|
2823
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-repeat-");
|
|
2824
|
+
try {
|
|
2825
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2826
|
+
await writeFile(
|
|
2827
|
+
join(cwd, "src", "runtime.ts"),
|
|
2828
|
+
[
|
|
2829
|
+
"export function loadRuntime() {",
|
|
2830
|
+
" // implement a quick hack fallback if it fails",
|
|
2831
|
+
" return process.env.RUNTIME || 'local';",
|
|
2832
|
+
"}",
|
|
2833
|
+
].join("\n"),
|
|
2834
|
+
);
|
|
2835
|
+
const payload = { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-repeat", turn_id: "turn-repeat" };
|
|
2836
|
+
|
|
2837
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
2838
|
+
const repeated = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
|
|
2839
|
+
|
|
2840
|
+
assert.equal((first.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2841
|
+
assert.equal((repeated.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2842
|
+
assert.equal((repeated.outputJson as { stopReason?: string } | null)?.stopReason, "sloppy_fallback_diff_audit");
|
|
2843
|
+
} finally {
|
|
2844
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
it("blocks Stop for unstaged tracked sloppy fallback source edits", async () => {
|
|
2849
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-unstaged-");
|
|
2566
2850
|
try {
|
|
2851
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2852
|
+
await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
|
|
2853
|
+
execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
|
|
2854
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
|
|
2855
|
+
await writeFile(
|
|
2856
|
+
join(cwd, "src", "runtime.ts"),
|
|
2857
|
+
[
|
|
2858
|
+
"export function loadRuntime() {",
|
|
2859
|
+
" // just bypass fallback if it fails",
|
|
2860
|
+
" return process.env.RUNTIME || 'local';",
|
|
2861
|
+
"}",
|
|
2862
|
+
].join("\n"),
|
|
2863
|
+
);
|
|
2864
|
+
|
|
2567
2865
|
const result = await dispatchCodexNativeHook(
|
|
2568
|
-
{
|
|
2569
|
-
hook_event_name: "PreToolUse",
|
|
2570
|
-
cwd,
|
|
2571
|
-
tool_name: "Bash",
|
|
2572
|
-
tool_use_id: "tool-git-commit-invalid",
|
|
2573
|
-
tool_input: { command: 'git commit -m "fix tests"' },
|
|
2574
|
-
},
|
|
2866
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-unstaged" },
|
|
2575
2867
|
{ cwd },
|
|
2576
2868
|
);
|
|
2577
2869
|
|
|
2578
|
-
assert.equal(result.omxEventName, "
|
|
2579
|
-
assert.
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2870
|
+
assert.equal(result.omxEventName, "stop");
|
|
2871
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2872
|
+
assert.match(JSON.stringify(result.outputJson), /unstaged/);
|
|
2873
|
+
} finally {
|
|
2874
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
it("blocks Stop from a subdirectory cwd for untracked sloppy source elsewhere", async () => {
|
|
2879
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-subdir-");
|
|
2880
|
+
try {
|
|
2881
|
+
await mkdir(join(cwd, "src", "nested"), { recursive: true });
|
|
2882
|
+
await writeFile(join(cwd, "src", "nested", "anchor.ts"), "export const anchor = true;\n");
|
|
2883
|
+
await writeFile(
|
|
2884
|
+
join(cwd, "src", "runtime.ts"),
|
|
2885
|
+
[
|
|
2886
|
+
"export function loadRuntime() {",
|
|
2887
|
+
" // implement a quick hack fallback if it fails",
|
|
2888
|
+
" return process.env.RUNTIME || 'local';",
|
|
2889
|
+
"}",
|
|
2592
2890
|
].join("\n"),
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2891
|
+
);
|
|
2892
|
+
|
|
2893
|
+
const subdir = join(cwd, "src", "nested");
|
|
2894
|
+
const result = await dispatchCodexNativeHook(
|
|
2895
|
+
{ hook_event_name: "Stop", cwd: subdir, session_id: "sess-stop-slop-subdir" },
|
|
2896
|
+
{ cwd: subdir },
|
|
2897
|
+
);
|
|
2898
|
+
|
|
2899
|
+
assert.equal(result.omxEventName, "stop");
|
|
2900
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2901
|
+
assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
|
|
2597
2902
|
} finally {
|
|
2598
2903
|
await rm(cwd, { recursive: true, force: true });
|
|
2599
2904
|
}
|
|
2600
2905
|
});
|
|
2601
2906
|
|
|
2602
|
-
it("
|
|
2603
|
-
const cwd = await
|
|
2907
|
+
it("blocks Stop for staged sloppy fallback source edits", async () => {
|
|
2908
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-staged-");
|
|
2604
2909
|
try {
|
|
2910
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2911
|
+
await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
|
|
2912
|
+
execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
|
|
2913
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
|
|
2914
|
+
await writeFile(
|
|
2915
|
+
join(cwd, "src", "runtime.ts"),
|
|
2916
|
+
[
|
|
2917
|
+
"export function loadRuntime() {",
|
|
2918
|
+
" // temporary workaround fallback if it fails",
|
|
2919
|
+
" return process.env.RUNTIME || 'local';",
|
|
2920
|
+
"}",
|
|
2921
|
+
].join("\n"),
|
|
2922
|
+
);
|
|
2923
|
+
execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
|
|
2924
|
+
|
|
2605
2925
|
const result = await dispatchCodexNativeHook(
|
|
2606
|
-
{
|
|
2607
|
-
hook_event_name: "PreToolUse",
|
|
2608
|
-
cwd,
|
|
2609
|
-
tool_name: "Bash",
|
|
2610
|
-
tool_use_id: "tool-git-help-commit",
|
|
2611
|
-
tool_input: { command: "git help commit" },
|
|
2612
|
-
},
|
|
2926
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-staged" },
|
|
2613
2927
|
{ cwd },
|
|
2614
2928
|
);
|
|
2615
2929
|
|
|
2616
|
-
assert.equal(result.omxEventName, "
|
|
2617
|
-
assert.equal(result.outputJson
|
|
2930
|
+
assert.equal(result.omxEventName, "stop");
|
|
2931
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
2932
|
+
assert.match(JSON.stringify(result.outputJson), /staged/);
|
|
2618
2933
|
} finally {
|
|
2619
2934
|
await rm(cwd, { recursive: true, force: true });
|
|
2620
2935
|
}
|
|
2621
2936
|
});
|
|
2622
2937
|
|
|
2623
|
-
it("
|
|
2624
|
-
const cwd = await
|
|
2938
|
+
it("does not block Stop for grounded compatibility fallback source edits", async () => {
|
|
2939
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-grounded-");
|
|
2625
2940
|
try {
|
|
2941
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2942
|
+
await writeFile(
|
|
2943
|
+
join(cwd, "src", "compat.ts"),
|
|
2944
|
+
[
|
|
2945
|
+
"export function resolveCompatMode() {",
|
|
2946
|
+
" // temporary fallback for legacy startup",
|
|
2947
|
+
" // compatibility fail-safe tested by regression coverage",
|
|
2948
|
+
" return 'legacy';",
|
|
2949
|
+
"}",
|
|
2950
|
+
].join("\n"),
|
|
2951
|
+
);
|
|
2952
|
+
|
|
2626
2953
|
const result = await dispatchCodexNativeHook(
|
|
2627
|
-
{
|
|
2628
|
-
hook_event_name: "PreToolUse",
|
|
2629
|
-
cwd,
|
|
2630
|
-
tool_name: "Bash",
|
|
2631
|
-
tool_use_id: "tool-git-config-alias-commit",
|
|
2632
|
-
tool_input: { command: "git config alias.ci commit" },
|
|
2633
|
-
},
|
|
2954
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-grounded" },
|
|
2634
2955
|
{ cwd },
|
|
2635
2956
|
);
|
|
2636
2957
|
|
|
2637
|
-
assert.equal(result.omxEventName, "
|
|
2958
|
+
assert.equal(result.omxEventName, "stop");
|
|
2638
2959
|
assert.equal(result.outputJson, null);
|
|
2639
2960
|
} finally {
|
|
2640
2961
|
await rm(cwd, { recursive: true, force: true });
|
|
2641
2962
|
}
|
|
2642
2963
|
});
|
|
2643
2964
|
|
|
2644
|
-
it("
|
|
2645
|
-
const cwd = await
|
|
2965
|
+
it("does not block Stop when existing nearby source context grounds a new fallback line", async () => {
|
|
2966
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-existing-ground-");
|
|
2646
2967
|
try {
|
|
2968
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
2969
|
+
await writeFile(
|
|
2970
|
+
join(cwd, "src", "compat.ts"),
|
|
2971
|
+
[
|
|
2972
|
+
"export function resolveCompatMode() {",
|
|
2973
|
+
" // compatibility fail-safe tested by regression coverage",
|
|
2974
|
+
" return 'legacy';",
|
|
2975
|
+
"}",
|
|
2976
|
+
].join("\n"),
|
|
2977
|
+
);
|
|
2978
|
+
execFileSync("git", ["add", "src/compat.ts"], { cwd, stdio: "ignore" });
|
|
2979
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
|
|
2980
|
+
await writeFile(
|
|
2981
|
+
join(cwd, "src", "compat.ts"),
|
|
2982
|
+
[
|
|
2983
|
+
"export function resolveCompatMode() {",
|
|
2984
|
+
" // compatibility fail-safe tested by regression coverage",
|
|
2985
|
+
" // temporary fallback if it fails",
|
|
2986
|
+
" return 'legacy';",
|
|
2987
|
+
"}",
|
|
2988
|
+
].join("\n"),
|
|
2989
|
+
);
|
|
2990
|
+
|
|
2647
2991
|
const result = await dispatchCodexNativeHook(
|
|
2648
|
-
{
|
|
2649
|
-
hook_event_name: "PreToolUse",
|
|
2650
|
-
cwd,
|
|
2651
|
-
tool_name: "Bash",
|
|
2652
|
-
tool_use_id: "tool-git-tag-commit",
|
|
2653
|
-
tool_input: { command: "git tag commit" },
|
|
2654
|
-
},
|
|
2992
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-existing-ground" },
|
|
2655
2993
|
{ cwd },
|
|
2656
2994
|
);
|
|
2657
2995
|
|
|
2658
|
-
assert.equal(result.omxEventName, "
|
|
2996
|
+
assert.equal(result.omxEventName, "stop");
|
|
2997
|
+
assert.equal(result.outputJson, null);
|
|
2998
|
+
} finally {
|
|
2999
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
|
|
3003
|
+
it("does not block Stop for source-adjacent test file fallback wording", async () => {
|
|
3004
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-test-file-");
|
|
3005
|
+
try {
|
|
3006
|
+
await mkdir(join(cwd, "src"), { recursive: true });
|
|
3007
|
+
await writeFile(
|
|
3008
|
+
join(cwd, "src", "runtime.test.ts"),
|
|
3009
|
+
"it('documents no quick hack fallback if it fails', () => {});\n",
|
|
3010
|
+
);
|
|
3011
|
+
|
|
3012
|
+
const result = await dispatchCodexNativeHook(
|
|
3013
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-test-file" },
|
|
3014
|
+
{ cwd },
|
|
3015
|
+
);
|
|
3016
|
+
|
|
3017
|
+
assert.equal(result.omxEventName, "stop");
|
|
3018
|
+
assert.equal(result.outputJson, null);
|
|
3019
|
+
} finally {
|
|
3020
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
|
|
3024
|
+
it("does not block Stop for docs-only fallback wording", async () => {
|
|
3025
|
+
const cwd = await initTempGitRepo("omx-native-hook-stop-slop-docs-");
|
|
3026
|
+
try {
|
|
3027
|
+
await mkdir(join(cwd, "docs"), { recursive: true });
|
|
3028
|
+
await writeFile(
|
|
3029
|
+
join(cwd, "docs", "notes.md"),
|
|
3030
|
+
"Do not implement a quick hack fallback if it fails.\n",
|
|
3031
|
+
);
|
|
3032
|
+
|
|
3033
|
+
const result = await dispatchCodexNativeHook(
|
|
3034
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-docs" },
|
|
3035
|
+
{ cwd },
|
|
3036
|
+
);
|
|
3037
|
+
|
|
3038
|
+
assert.equal(result.omxEventName, "stop");
|
|
3039
|
+
assert.equal(result.outputJson, null);
|
|
3040
|
+
} finally {
|
|
3041
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
|
|
3045
|
+
it("keeps git commit Lore enforcement ahead of sloppy fallback advisory", async () => {
|
|
3046
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-git-priority-"));
|
|
3047
|
+
try {
|
|
3048
|
+
const result = await dispatchCodexNativeHook(
|
|
3049
|
+
{
|
|
3050
|
+
hook_event_name: "PreToolUse",
|
|
3051
|
+
cwd,
|
|
3052
|
+
tool_name: "Bash",
|
|
3053
|
+
tool_use_id: "tool-slop-git-priority",
|
|
3054
|
+
tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
|
|
3055
|
+
},
|
|
3056
|
+
{ cwd },
|
|
3057
|
+
);
|
|
3058
|
+
|
|
3059
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3060
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3061
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3062
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /don't make potential slop/);
|
|
3063
|
+
} finally {
|
|
3064
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
|
|
3069
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
|
|
3070
|
+
try {
|
|
3071
|
+
const result = await dispatchCodexNativeHook(
|
|
3072
|
+
{
|
|
3073
|
+
hook_event_name: "PreToolUse",
|
|
3074
|
+
cwd,
|
|
3075
|
+
tool_name: "Bash",
|
|
3076
|
+
tool_use_id: "tool-git-commit-invalid",
|
|
3077
|
+
tool_input: { command: 'git commit -m "fix tests"' },
|
|
3078
|
+
},
|
|
3079
|
+
{ cwd },
|
|
3080
|
+
);
|
|
3081
|
+
|
|
3082
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3083
|
+
assert.deepEqual(result.outputJson, {
|
|
3084
|
+
decision: "block",
|
|
3085
|
+
reason:
|
|
3086
|
+
"git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
|
|
3087
|
+
hookSpecificOutput: {
|
|
3088
|
+
hookEventName: "PreToolUse",
|
|
3089
|
+
},
|
|
3090
|
+
systemMessage: [
|
|
3091
|
+
"git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
3092
|
+
"- Add a blank line after the subject before the narrative body.",
|
|
3093
|
+
"- Add a narrative body paragraph explaining the decision context.",
|
|
3094
|
+
"- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
|
|
3095
|
+
"- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
|
|
3096
|
+
].join("\n"),
|
|
3097
|
+
});
|
|
3098
|
+
const hookSpecificOutput = (result.outputJson as { hookSpecificOutput?: Record<string, unknown> })
|
|
3099
|
+
.hookSpecificOutput ?? {};
|
|
3100
|
+
assert.equal("additionalContext" in hookSpecificOutput, false);
|
|
3101
|
+
} finally {
|
|
3102
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
|
|
3106
|
+
it("allows non-Lore git commit messages when the Lore commit guard is explicitly disabled", async () => {
|
|
3107
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
|
|
3108
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3109
|
+
try {
|
|
3110
|
+
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
3111
|
+
const result = await dispatchCodexNativeHook(
|
|
3112
|
+
{
|
|
3113
|
+
hook_event_name: "PreToolUse",
|
|
3114
|
+
cwd,
|
|
3115
|
+
tool_name: "Bash",
|
|
3116
|
+
tool_use_id: "tool-git-commit-lore-disabled",
|
|
3117
|
+
tool_input: { command: 'git commit -m "fix: use conventional commit"' },
|
|
3118
|
+
},
|
|
3119
|
+
{ cwd },
|
|
3120
|
+
);
|
|
3121
|
+
|
|
3122
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3123
|
+
assert.equal(result.outputJson, null);
|
|
3124
|
+
} finally {
|
|
3125
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3126
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3127
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3128
|
+
}
|
|
3129
|
+
});
|
|
3130
|
+
|
|
3131
|
+
it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
|
|
3132
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
|
|
3133
|
+
try {
|
|
3134
|
+
const result = await dispatchCodexNativeHook(
|
|
3135
|
+
{
|
|
3136
|
+
hook_event_name: "PreToolUse",
|
|
3137
|
+
cwd,
|
|
3138
|
+
tool_name: "Bash",
|
|
3139
|
+
tool_use_id: "tool-git-commit-lore-inline-disabled",
|
|
3140
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
|
|
3141
|
+
},
|
|
3142
|
+
{ cwd },
|
|
3143
|
+
);
|
|
3144
|
+
|
|
3145
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3146
|
+
assert.equal(result.outputJson, null);
|
|
3147
|
+
} finally {
|
|
3148
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
|
|
3152
|
+
it("does not treat newline-separated Lore guard assignment as inline git commit env", async () => {
|
|
3153
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
|
|
3154
|
+
try {
|
|
3155
|
+
const result = await dispatchCodexNativeHook(
|
|
3156
|
+
{
|
|
3157
|
+
hook_event_name: "PreToolUse",
|
|
3158
|
+
cwd,
|
|
3159
|
+
tool_name: "Bash",
|
|
3160
|
+
tool_use_id: "tool-git-commit-lore-newline-assignment",
|
|
3161
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0\ngit commit -m "fix: conventional"' },
|
|
3162
|
+
},
|
|
3163
|
+
{ cwd },
|
|
3164
|
+
);
|
|
3165
|
+
|
|
3166
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3167
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3168
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3169
|
+
} finally {
|
|
3170
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
|
|
3174
|
+
it("restores default-on Lore guard when env -u unsets a disabled process env", async () => {
|
|
3175
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
|
|
3176
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3177
|
+
try {
|
|
3178
|
+
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
3179
|
+
const result = await dispatchCodexNativeHook(
|
|
3180
|
+
{
|
|
3181
|
+
hook_event_name: "PreToolUse",
|
|
3182
|
+
cwd,
|
|
3183
|
+
tool_name: "Bash",
|
|
3184
|
+
tool_use_id: "tool-git-commit-lore-env-unset",
|
|
3185
|
+
tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
|
|
3186
|
+
},
|
|
3187
|
+
{ cwd },
|
|
3188
|
+
);
|
|
3189
|
+
|
|
3190
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3191
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3192
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3193
|
+
} finally {
|
|
3194
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3195
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3196
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3197
|
+
}
|
|
3198
|
+
});
|
|
3199
|
+
|
|
3200
|
+
it("restores default-on Lore guard when env -i clears a disabled process env", async () => {
|
|
3201
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
|
|
3202
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3203
|
+
try {
|
|
3204
|
+
process.env.OMX_LORE_COMMIT_GUARD = "0";
|
|
3205
|
+
const result = await dispatchCodexNativeHook(
|
|
3206
|
+
{
|
|
3207
|
+
hook_event_name: "PreToolUse",
|
|
3208
|
+
cwd,
|
|
3209
|
+
tool_name: "Bash",
|
|
3210
|
+
tool_use_id: "tool-git-commit-lore-env-ignore",
|
|
3211
|
+
tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
|
|
3212
|
+
},
|
|
3213
|
+
{ cwd },
|
|
3214
|
+
);
|
|
3215
|
+
|
|
3216
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3217
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3218
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3219
|
+
} finally {
|
|
3220
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3221
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3222
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
|
|
3226
|
+
it("keeps Lore commit enforcement enabled for unknown inline guard values", async () => {
|
|
3227
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
|
|
3228
|
+
try {
|
|
3229
|
+
const result = await dispatchCodexNativeHook(
|
|
3230
|
+
{
|
|
3231
|
+
hook_event_name: "PreToolUse",
|
|
3232
|
+
cwd,
|
|
3233
|
+
tool_name: "Bash",
|
|
3234
|
+
tool_use_id: "tool-git-commit-lore-inline-unknown",
|
|
3235
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
|
|
3236
|
+
},
|
|
3237
|
+
{ cwd },
|
|
3238
|
+
);
|
|
3239
|
+
|
|
3240
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3241
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3242
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3243
|
+
} finally {
|
|
3244
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3245
|
+
}
|
|
3246
|
+
});
|
|
3247
|
+
|
|
3248
|
+
it("treats Lore commit guard disabled values as trim and case tolerant", async () => {
|
|
3249
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-off-"));
|
|
3250
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3251
|
+
try {
|
|
3252
|
+
process.env.OMX_LORE_COMMIT_GUARD = " OFF ";
|
|
3253
|
+
const result = await dispatchCodexNativeHook(
|
|
3254
|
+
{
|
|
3255
|
+
hook_event_name: "PreToolUse",
|
|
3256
|
+
cwd,
|
|
3257
|
+
tool_name: "Bash",
|
|
3258
|
+
tool_use_id: "tool-git-commit-lore-off",
|
|
3259
|
+
tool_input: { command: 'git commit -m "chore: conventional commit"' },
|
|
3260
|
+
},
|
|
3261
|
+
{ cwd },
|
|
3262
|
+
);
|
|
3263
|
+
|
|
3264
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3265
|
+
assert.equal(result.outputJson, null);
|
|
3266
|
+
} finally {
|
|
3267
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3268
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3269
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
|
|
3273
|
+
it("keeps Lore commit enforcement enabled for unknown guard values", async () => {
|
|
3274
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
|
|
3275
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3276
|
+
try {
|
|
3277
|
+
process.env.OMX_LORE_COMMIT_GUARD = "maybe";
|
|
3278
|
+
const result = await dispatchCodexNativeHook(
|
|
3279
|
+
{
|
|
3280
|
+
hook_event_name: "PreToolUse",
|
|
3281
|
+
cwd,
|
|
3282
|
+
tool_name: "Bash",
|
|
3283
|
+
tool_use_id: "tool-git-commit-lore-unknown",
|
|
3284
|
+
tool_input: { command: 'git commit -m "fix tests"' },
|
|
3285
|
+
},
|
|
3286
|
+
{ cwd },
|
|
3287
|
+
);
|
|
3288
|
+
|
|
3289
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3290
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
3291
|
+
assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3292
|
+
} finally {
|
|
3293
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3294
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3295
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3296
|
+
}
|
|
3297
|
+
});
|
|
3298
|
+
|
|
3299
|
+
it("continues to later PreToolUse checks when Lore commit guard is disabled", async () => {
|
|
3300
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-lore-disabled-destructive-"));
|
|
3301
|
+
const original = process.env.OMX_LORE_COMMIT_GUARD;
|
|
3302
|
+
try {
|
|
3303
|
+
process.env.OMX_LORE_COMMIT_GUARD = "false";
|
|
3304
|
+
const result = await dispatchCodexNativeHook(
|
|
3305
|
+
{
|
|
3306
|
+
hook_event_name: "PreToolUse",
|
|
3307
|
+
cwd,
|
|
3308
|
+
tool_name: "Bash",
|
|
3309
|
+
tool_use_id: "tool-lore-disabled-destructive",
|
|
3310
|
+
tool_input: { command: "rm -rf dist" },
|
|
3311
|
+
},
|
|
3312
|
+
{ cwd },
|
|
3313
|
+
);
|
|
3314
|
+
|
|
3315
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3316
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /Lore protocol/);
|
|
3317
|
+
assert.match(JSON.stringify(result.outputJson), /Destructive Bash command detected/);
|
|
3318
|
+
} finally {
|
|
3319
|
+
if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
|
|
3320
|
+
else process.env.OMX_LORE_COMMIT_GUARD = original;
|
|
3321
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
it("stays silent on PreToolUse for `git help commit`", async () => {
|
|
3326
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
|
|
3327
|
+
try {
|
|
3328
|
+
const result = await dispatchCodexNativeHook(
|
|
3329
|
+
{
|
|
3330
|
+
hook_event_name: "PreToolUse",
|
|
3331
|
+
cwd,
|
|
3332
|
+
tool_name: "Bash",
|
|
3333
|
+
tool_use_id: "tool-git-help-commit",
|
|
3334
|
+
tool_input: { command: "git help commit" },
|
|
3335
|
+
},
|
|
3336
|
+
{ cwd },
|
|
3337
|
+
);
|
|
3338
|
+
|
|
3339
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3340
|
+
assert.equal(result.outputJson, null);
|
|
3341
|
+
} finally {
|
|
3342
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3343
|
+
}
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
|
|
3347
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
|
|
3348
|
+
try {
|
|
3349
|
+
const result = await dispatchCodexNativeHook(
|
|
3350
|
+
{
|
|
3351
|
+
hook_event_name: "PreToolUse",
|
|
3352
|
+
cwd,
|
|
3353
|
+
tool_name: "Bash",
|
|
3354
|
+
tool_use_id: "tool-git-config-alias-commit",
|
|
3355
|
+
tool_input: { command: "git config alias.ci commit" },
|
|
3356
|
+
},
|
|
3357
|
+
{ cwd },
|
|
3358
|
+
);
|
|
3359
|
+
|
|
3360
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
3361
|
+
assert.equal(result.outputJson, null);
|
|
3362
|
+
} finally {
|
|
3363
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
it("stays silent on PreToolUse for `git tag commit`", async () => {
|
|
3368
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
|
|
3369
|
+
try {
|
|
3370
|
+
const result = await dispatchCodexNativeHook(
|
|
3371
|
+
{
|
|
3372
|
+
hook_event_name: "PreToolUse",
|
|
3373
|
+
cwd,
|
|
3374
|
+
tool_name: "Bash",
|
|
3375
|
+
tool_use_id: "tool-git-tag-commit",
|
|
3376
|
+
tool_input: { command: "git tag commit" },
|
|
3377
|
+
},
|
|
3378
|
+
{ cwd },
|
|
3379
|
+
);
|
|
3380
|
+
|
|
3381
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2659
3382
|
assert.equal(result.outputJson, null);
|
|
2660
3383
|
} finally {
|
|
2661
3384
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -3837,12 +4560,49 @@ esac
|
|
|
3837
4560
|
}
|
|
3838
4561
|
});
|
|
3839
4562
|
|
|
4563
|
+
for (const rootActiveCase of [
|
|
4564
|
+
{ mode: "autopilot", phase: "execution" },
|
|
4565
|
+
{ mode: "ultrawork", phase: "executing" },
|
|
4566
|
+
{ mode: "ultraqa", phase: "diagnose" },
|
|
4567
|
+
] as const) {
|
|
4568
|
+
it(`returns Stop continuation output from root ${rootActiveCase.mode} state when no session is active`, async () => {
|
|
4569
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-root-${rootActiveCase.mode}-`));
|
|
4570
|
+
try {
|
|
4571
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4572
|
+
await mkdir(stateDir, { recursive: true });
|
|
4573
|
+
await writeJson(join(stateDir, `${rootActiveCase.mode}-state.json`), {
|
|
4574
|
+
active: true,
|
|
4575
|
+
mode: rootActiveCase.mode,
|
|
4576
|
+
current_phase: rootActiveCase.phase,
|
|
4577
|
+
});
|
|
4578
|
+
|
|
4579
|
+
const result = await dispatchCodexNativeHook(
|
|
4580
|
+
{
|
|
4581
|
+
hook_event_name: "Stop",
|
|
4582
|
+
cwd,
|
|
4583
|
+
},
|
|
4584
|
+
{ cwd },
|
|
4585
|
+
);
|
|
4586
|
+
|
|
4587
|
+
assert.equal(result.omxEventName, "stop");
|
|
4588
|
+
assert.deepEqual(result.outputJson, {
|
|
4589
|
+
decision: "block",
|
|
4590
|
+
reason: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}); continue the task and gather fresh verification evidence before stopping.`,
|
|
4591
|
+
stopReason: `${rootActiveCase.mode}_${rootActiveCase.phase}`,
|
|
4592
|
+
systemMessage: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}).`,
|
|
4593
|
+
});
|
|
4594
|
+
} finally {
|
|
4595
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4596
|
+
}
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
|
|
3840
4600
|
it("returns Stop continuation output while Autopilot is active", async () => {
|
|
3841
4601
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-"));
|
|
3842
4602
|
try {
|
|
3843
4603
|
const stateDir = join(cwd, ".omx", "state");
|
|
3844
|
-
await mkdir(stateDir, { recursive: true });
|
|
3845
|
-
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
4604
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autopilot"), { recursive: true });
|
|
4605
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autopilot", "autopilot-state.json"), {
|
|
3846
4606
|
active: true,
|
|
3847
4607
|
current_phase: "execution",
|
|
3848
4608
|
});
|
|
@@ -3873,8 +4633,8 @@ esac
|
|
|
3873
4633
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
|
|
3874
4634
|
try {
|
|
3875
4635
|
const stateDir = join(cwd, ".omx", "state");
|
|
3876
|
-
await mkdir(stateDir, { recursive: true });
|
|
3877
|
-
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
4636
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay"), { recursive: true });
|
|
4637
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay", "autopilot-state.json"), {
|
|
3878
4638
|
active: true,
|
|
3879
4639
|
current_phase: "planning",
|
|
3880
4640
|
});
|
|
@@ -3939,12 +4699,45 @@ esac
|
|
|
3939
4699
|
}
|
|
3940
4700
|
});
|
|
3941
4701
|
|
|
4702
|
+
for (const staleRootCase of [
|
|
4703
|
+
{ mode: "autopilot", phase: "execution" },
|
|
4704
|
+
{ mode: "ultrawork", phase: "executing" },
|
|
4705
|
+
{ mode: "ultraqa", phase: "diagnose" },
|
|
4706
|
+
] as const) {
|
|
4707
|
+
it(`does not block Stop from stale root ${staleRootCase.mode} state when the explicit session directory is missing`, async () => {
|
|
4708
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-missing-session-${staleRootCase.mode}-`));
|
|
4709
|
+
try {
|
|
4710
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4711
|
+
await mkdir(stateDir, { recursive: true });
|
|
4712
|
+
await writeJson(join(stateDir, `${staleRootCase.mode}-state.json`), {
|
|
4713
|
+
active: true,
|
|
4714
|
+
mode: staleRootCase.mode,
|
|
4715
|
+
current_phase: staleRootCase.phase,
|
|
4716
|
+
});
|
|
4717
|
+
|
|
4718
|
+
const result = await dispatchCodexNativeHook(
|
|
4719
|
+
{
|
|
4720
|
+
hook_event_name: "Stop",
|
|
4721
|
+
cwd,
|
|
4722
|
+
session_id: "missing-session",
|
|
4723
|
+
},
|
|
4724
|
+
{ cwd },
|
|
4725
|
+
);
|
|
4726
|
+
|
|
4727
|
+
assert.equal(result.omxEventName, "stop");
|
|
4728
|
+
assert.equal(result.outputJson, null);
|
|
4729
|
+
} finally {
|
|
4730
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4731
|
+
}
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
|
|
3942
4735
|
it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
|
|
3943
4736
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
|
|
3944
4737
|
try {
|
|
3945
4738
|
const stateDir = join(cwd, ".omx", "state");
|
|
3946
|
-
await mkdir(stateDir, { recursive: true });
|
|
3947
|
-
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
4739
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome"), { recursive: true });
|
|
4740
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome", "autopilot-state.json"), {
|
|
3948
4741
|
active: true,
|
|
3949
4742
|
current_phase: "execution",
|
|
3950
4743
|
run_outcome: "blocked_on_user",
|
|
@@ -3970,8 +4763,8 @@ esac
|
|
|
3970
4763
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
|
|
3971
4764
|
try {
|
|
3972
4765
|
const stateDir = join(cwd, ".omx", "state");
|
|
3973
|
-
await mkdir(stateDir, { recursive: true });
|
|
3974
|
-
await writeJson(join(stateDir, "ultrawork-state.json"), {
|
|
4766
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork"), { recursive: true });
|
|
4767
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork", "ultrawork-state.json"), {
|
|
3975
4768
|
active: true,
|
|
3976
4769
|
current_phase: "executing",
|
|
3977
4770
|
});
|
|
@@ -3997,8 +4790,8 @@ esac
|
|
|
3997
4790
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultraqa-"));
|
|
3998
4791
|
try {
|
|
3999
4792
|
const stateDir = join(cwd, ".omx", "state");
|
|
4000
|
-
await mkdir(stateDir, { recursive: true });
|
|
4001
|
-
await writeJson(join(stateDir, "ultraqa-state.json"), {
|
|
4793
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultraqa"), { recursive: true });
|
|
4794
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultraqa", "ultraqa-state.json"), {
|
|
4002
4795
|
active: true,
|
|
4003
4796
|
current_phase: "diagnose",
|
|
4004
4797
|
});
|
|
@@ -4020,6 +4813,46 @@ esac
|
|
|
4020
4813
|
}
|
|
4021
4814
|
});
|
|
4022
4815
|
|
|
4816
|
+
it("marks leader-owned team attention during native Stop dispatch without a polling watcher", async () => {
|
|
4817
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-attention-"));
|
|
4818
|
+
try {
|
|
4819
|
+
await initTeamState(
|
|
4820
|
+
"stop-attention-team",
|
|
4821
|
+
"native stop attention",
|
|
4822
|
+
"executor",
|
|
4823
|
+
1,
|
|
4824
|
+
cwd,
|
|
4825
|
+
undefined,
|
|
4826
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-attention" },
|
|
4827
|
+
);
|
|
4828
|
+
|
|
4829
|
+
const result = await dispatchCodexNativeHook(
|
|
4830
|
+
{
|
|
4831
|
+
hook_event_name: "Stop",
|
|
4832
|
+
cwd,
|
|
4833
|
+
session_id: "sess-stop-team-attention",
|
|
4834
|
+
},
|
|
4835
|
+
{ cwd },
|
|
4836
|
+
);
|
|
4837
|
+
|
|
4838
|
+
const attention = await readTeamLeaderAttention("stop-attention-team", cwd);
|
|
4839
|
+
assert.equal(result.omxEventName, "stop");
|
|
4840
|
+
assert.equal(attention?.source, "native_stop");
|
|
4841
|
+
assert.equal(attention?.leader_session_active, false);
|
|
4842
|
+
assert.equal(attention?.leader_session_id, "sess-stop-team-attention");
|
|
4843
|
+
assert.match(attention?.leader_session_stopped_at ?? "", /^\d{4}-\d{2}-\d{2}T/);
|
|
4844
|
+
assert.deepEqual(result.outputJson, {
|
|
4845
|
+
decision: "block",
|
|
4846
|
+
reason:
|
|
4847
|
+
`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}`,
|
|
4848
|
+
stopReason: "team_team-exec",
|
|
4849
|
+
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
4850
|
+
});
|
|
4851
|
+
} finally {
|
|
4852
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4853
|
+
}
|
|
4854
|
+
});
|
|
4855
|
+
|
|
4023
4856
|
it("returns Stop continuation output while team phase is non-terminal", async () => {
|
|
4024
4857
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-"));
|
|
4025
4858
|
try {
|
|
@@ -4131,7 +4964,7 @@ esac
|
|
|
4131
4964
|
}
|
|
4132
4965
|
});
|
|
4133
4966
|
|
|
4134
|
-
it("
|
|
4967
|
+
it("blocks Stop as a team-worker task failure when worker status is terminal but task evidence is not completed", async () => {
|
|
4135
4968
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-stale-"));
|
|
4136
4969
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
4137
4970
|
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
@@ -4166,7 +4999,7 @@ esac
|
|
|
4166
4999
|
await writeJson(join(stateDir, "team", "worker-stale-team", "tasks", "task-1.json"), {
|
|
4167
5000
|
id: "1",
|
|
4168
5001
|
subject: "stale hook task",
|
|
4169
|
-
description: "
|
|
5002
|
+
description: "non-completed task should still block terminal worker Stop",
|
|
4170
5003
|
status: "in_progress",
|
|
4171
5004
|
owner: "worker-1",
|
|
4172
5005
|
created_at: new Date().toISOString(),
|
|
@@ -4176,16 +5009,23 @@ esac
|
|
|
4176
5009
|
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
4177
5010
|
process.env.OMX_TEAM_LEADER_CWD = cwd;
|
|
4178
5011
|
|
|
4179
|
-
const
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
5012
|
+
const payload = {
|
|
5013
|
+
hook_event_name: "Stop",
|
|
5014
|
+
cwd: workerCwd,
|
|
5015
|
+
session_id: "sess-stop-team-worker-stale",
|
|
5016
|
+
thread_id: "thread-stop-team-worker-stale",
|
|
5017
|
+
};
|
|
5018
|
+
const result = await dispatchCodexNativeHook(payload, { cwd: workerCwd });
|
|
5019
|
+
const replay = await dispatchCodexNativeHook(
|
|
5020
|
+
{ ...payload, stop_hook_active: true },
|
|
4185
5021
|
{ cwd: workerCwd },
|
|
4186
5022
|
);
|
|
4187
5023
|
|
|
4188
|
-
assert.equal(
|
|
5024
|
+
assert.equal(
|
|
5025
|
+
(result.outputJson as { stopReason?: string } | null)?.stopReason,
|
|
5026
|
+
"team_worker_worker-1_1_in_progress",
|
|
5027
|
+
);
|
|
5028
|
+
assert.equal(replay.outputJson, null);
|
|
4189
5029
|
} finally {
|
|
4190
5030
|
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4191
5031
|
else delete process.env.OMX_TEAM_WORKER;
|
|
@@ -4197,7 +5037,7 @@ esac
|
|
|
4197
5037
|
}
|
|
4198
5038
|
});
|
|
4199
5039
|
|
|
4200
|
-
it("
|
|
5040
|
+
it("re-blocks live team worker Stop replays but suppresses stale terminal worker repeats", async () => {
|
|
4201
5041
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
|
|
4202
5042
|
try {
|
|
4203
5043
|
await initTeamState(
|
|
@@ -4268,85 +5108,516 @@ esac
|
|
|
4268
5108
|
|
|
4269
5109
|
await writeJson(taskPath, {
|
|
4270
5110
|
id: "1",
|
|
4271
|
-
subject: "hook task",
|
|
4272
|
-
description: "finish hook task",
|
|
4273
|
-
status: "blocked",
|
|
5111
|
+
subject: "hook task",
|
|
5112
|
+
description: "finish hook task",
|
|
5113
|
+
status: "blocked",
|
|
5114
|
+
owner: "worker-1",
|
|
5115
|
+
created_at: new Date().toISOString(),
|
|
5116
|
+
});
|
|
5117
|
+
const stateChanged = await dispatchCodexNativeHook(
|
|
5118
|
+
{ ...basePayload, turn_id: "turn-stop-team-worker-repeat-3", stop_hook_active: true },
|
|
5119
|
+
{ cwd: workerCwd },
|
|
5120
|
+
);
|
|
5121
|
+
|
|
5122
|
+
assert.deepEqual(first.outputJson, expectedInProgress);
|
|
5123
|
+
assert.deepEqual(replay.outputJson, expectedInProgress);
|
|
5124
|
+
assert.deepEqual(freshTurn.outputJson, expectedInProgress);
|
|
5125
|
+
assert.deepEqual(stateChanged.outputJson, {
|
|
5126
|
+
decision: "block",
|
|
5127
|
+
reason:
|
|
5128
|
+
"OMX team worker worker-1 is still assigned non-terminal task 1 (blocked); continue the current assigned task or report a concrete blocker before stopping.",
|
|
5129
|
+
stopReason: "team_worker_worker-1_1_blocked",
|
|
5130
|
+
systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
|
|
5131
|
+
});
|
|
5132
|
+
} finally {
|
|
5133
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5134
|
+
}
|
|
5135
|
+
});
|
|
5136
|
+
|
|
5137
|
+
it("allows Stop for a team worker when assigned task is terminal and bypasses generic team blocking", async () => {
|
|
5138
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
|
|
5139
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5140
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5141
|
+
const prevPath = process.env.PATH;
|
|
5142
|
+
try {
|
|
5143
|
+
await initTeamState(
|
|
5144
|
+
"worker-stop-team-terminal",
|
|
5145
|
+
"worker stop terminal fallback",
|
|
5146
|
+
"executor",
|
|
5147
|
+
1,
|
|
5148
|
+
cwd,
|
|
5149
|
+
undefined,
|
|
5150
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" },
|
|
5151
|
+
);
|
|
5152
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5153
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
5154
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5155
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
5156
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5157
|
+
const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
|
|
5158
|
+
await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "config.json"), {
|
|
5159
|
+
name: "worker-stop-team-terminal",
|
|
5160
|
+
tmux_session: "omx-team-worker-stop",
|
|
5161
|
+
leader_pane_id: "%42",
|
|
5162
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5163
|
+
});
|
|
5164
|
+
await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "manifest.v2.json"), {
|
|
5165
|
+
name: "worker-stop-team-terminal",
|
|
5166
|
+
tmux_session: "omx-team-worker-stop",
|
|
5167
|
+
leader_pane_id: "%42",
|
|
5168
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5169
|
+
});
|
|
5170
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5171
|
+
name: "worker-1",
|
|
5172
|
+
index: 1,
|
|
5173
|
+
role: "executor",
|
|
5174
|
+
assigned_tasks: ["1"],
|
|
5175
|
+
worktree_path: cwd,
|
|
5176
|
+
team_state_root: join(cwd, ".omx", "state"),
|
|
5177
|
+
});
|
|
5178
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
5179
|
+
state: "done",
|
|
5180
|
+
current_task_id: "1",
|
|
5181
|
+
updated_at: new Date().toISOString(),
|
|
5182
|
+
});
|
|
5183
|
+
await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "tasks", "task-1.json"), {
|
|
5184
|
+
id: "1",
|
|
5185
|
+
subject: "hook task",
|
|
5186
|
+
description: "finish hook task",
|
|
5187
|
+
status: "completed",
|
|
5188
|
+
owner: "worker-1",
|
|
5189
|
+
created_at: new Date().toISOString(),
|
|
5190
|
+
});
|
|
5191
|
+
|
|
5192
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
|
|
5193
|
+
process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
|
|
5194
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
5195
|
+
|
|
5196
|
+
const result = await dispatchCodexNativeHook(
|
|
5197
|
+
{
|
|
5198
|
+
hook_event_name: "Stop",
|
|
5199
|
+
cwd,
|
|
5200
|
+
session_id: "sess-stop-team-worker-terminal",
|
|
5201
|
+
},
|
|
5202
|
+
{ cwd },
|
|
5203
|
+
);
|
|
5204
|
+
const replay = await dispatchCodexNativeHook(
|
|
5205
|
+
{
|
|
5206
|
+
hook_event_name: "Stop",
|
|
5207
|
+
cwd,
|
|
5208
|
+
session_id: "sess-stop-team-worker-terminal",
|
|
5209
|
+
turn_id: "turn-worker-stop-terminal-replay",
|
|
5210
|
+
},
|
|
5211
|
+
{ cwd },
|
|
5212
|
+
);
|
|
5213
|
+
|
|
5214
|
+
assert.equal(result.outputJson, null);
|
|
5215
|
+
assert.equal(replay.outputJson, null);
|
|
5216
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
5217
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/g) || [];
|
|
5218
|
+
assert.equal(stopNudges.length, 1, "allowed worker Stop should nudge leader exactly once inside cooldown");
|
|
5219
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
5220
|
+
assert.equal(nudgeState.delivery, "sent");
|
|
5221
|
+
} finally {
|
|
5222
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5223
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
5224
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5225
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5226
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
5227
|
+
else delete process.env.PATH;
|
|
5228
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5229
|
+
}
|
|
5230
|
+
});
|
|
5231
|
+
|
|
5232
|
+
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
5233
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
5234
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5235
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5236
|
+
const prevPath = process.env.PATH;
|
|
5237
|
+
try {
|
|
5238
|
+
await initTeamState(
|
|
5239
|
+
"worker-stop-helper-fail",
|
|
5240
|
+
"worker stop helper failure",
|
|
5241
|
+
"executor",
|
|
5242
|
+
1,
|
|
5243
|
+
cwd,
|
|
5244
|
+
undefined,
|
|
5245
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" },
|
|
5246
|
+
);
|
|
5247
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5248
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5249
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
|
|
5250
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5251
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5252
|
+
const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
|
|
5253
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
|
|
5254
|
+
name: "worker-stop-helper-fail",
|
|
5255
|
+
tmux_session: "omx-team-worker-stop",
|
|
5256
|
+
leader_pane_id: "%42",
|
|
5257
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5258
|
+
});
|
|
5259
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
|
|
5260
|
+
name: "worker-stop-helper-fail",
|
|
5261
|
+
tmux_session: "omx-team-worker-stop",
|
|
5262
|
+
leader_pane_id: "%42",
|
|
5263
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5264
|
+
});
|
|
5265
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5266
|
+
name: "worker-1",
|
|
5267
|
+
assigned_tasks: ["1"],
|
|
5268
|
+
team_state_root: stateDir,
|
|
5269
|
+
});
|
|
5270
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
5271
|
+
state: "done",
|
|
5272
|
+
current_task_id: "1",
|
|
5273
|
+
updated_at: new Date().toISOString(),
|
|
5274
|
+
});
|
|
5275
|
+
await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
|
|
5276
|
+
id: "1",
|
|
5277
|
+
status: "completed",
|
|
5278
|
+
owner: "worker-1",
|
|
5279
|
+
});
|
|
5280
|
+
|
|
5281
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
|
|
5282
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5283
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
5284
|
+
|
|
5285
|
+
const result = await dispatchCodexNativeHook(
|
|
5286
|
+
{ hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" },
|
|
5287
|
+
{ cwd },
|
|
5288
|
+
);
|
|
5289
|
+
|
|
5290
|
+
assert.equal(result.outputJson, null);
|
|
5291
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
5292
|
+
assert.equal(nudgeState.delivery, "deferred");
|
|
5293
|
+
} finally {
|
|
5294
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5295
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
5296
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5297
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5298
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
5299
|
+
else delete process.env.PATH;
|
|
5300
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5301
|
+
}
|
|
5302
|
+
});
|
|
5303
|
+
|
|
5304
|
+
it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
|
|
5305
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
|
|
5306
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5307
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5308
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5309
|
+
const prevPath = process.env.PATH;
|
|
5310
|
+
try {
|
|
5311
|
+
await initTeamState(
|
|
5312
|
+
"worker-stop-failed-task",
|
|
5313
|
+
"worker stop failed task",
|
|
5314
|
+
"executor",
|
|
5315
|
+
1,
|
|
5316
|
+
cwd,
|
|
5317
|
+
undefined,
|
|
5318
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" },
|
|
5319
|
+
);
|
|
5320
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5321
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
5322
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5323
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
5324
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5325
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5326
|
+
const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
|
|
5327
|
+
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
|
|
5328
|
+
name: "worker-stop-failed-task",
|
|
5329
|
+
tmux_session: "omx-team-worker-stop",
|
|
5330
|
+
leader_pane_id: "%42",
|
|
5331
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5332
|
+
});
|
|
5333
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5334
|
+
name: "worker-1",
|
|
5335
|
+
assigned_tasks: ["1"],
|
|
5336
|
+
team_state_root: stateDir,
|
|
5337
|
+
});
|
|
5338
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
5339
|
+
state: "failed",
|
|
5340
|
+
current_task_id: "1",
|
|
5341
|
+
updated_at: new Date().toISOString(),
|
|
5342
|
+
});
|
|
5343
|
+
await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
|
|
5344
|
+
id: "1",
|
|
5345
|
+
status: "failed",
|
|
5346
|
+
owner: "worker-1",
|
|
5347
|
+
});
|
|
5348
|
+
|
|
5349
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
|
|
5350
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5351
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5352
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
5353
|
+
|
|
5354
|
+
const result = await dispatchCodexNativeHook(
|
|
5355
|
+
{
|
|
5356
|
+
hook_event_name: "Stop",
|
|
5357
|
+
cwd,
|
|
5358
|
+
session_id: "sess-stop-team-worker-failed",
|
|
5359
|
+
thread_id: "thread-stop-team-worker-failed",
|
|
5360
|
+
turn_id: "turn-stop-team-worker-failed",
|
|
5361
|
+
},
|
|
5362
|
+
{ cwd },
|
|
5363
|
+
);
|
|
5364
|
+
|
|
5365
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
5366
|
+
assert.match(String(result.outputJson?.stopReason || ""), /non_completed_task_1_failed/);
|
|
5367
|
+
assert.match(JSON.stringify(result.outputJson), /team/i);
|
|
5368
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
|
|
5369
|
+
const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
|
|
5370
|
+
assert.doesNotMatch(tmuxLog, /native Stop allowed/);
|
|
5371
|
+
} finally {
|
|
5372
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5373
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
5374
|
+
if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
5375
|
+
else delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5376
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5377
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5378
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
5379
|
+
else delete process.env.PATH;
|
|
5380
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5381
|
+
}
|
|
5382
|
+
});
|
|
5383
|
+
|
|
5384
|
+
it("blocks worker Stop on missing task assignment without relying on generic team state", async () => {
|
|
5385
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-assignment-"));
|
|
5386
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5387
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5388
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5389
|
+
try {
|
|
5390
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5391
|
+
const workerDir = join(stateDir, "team", "worker-missing-assignment", "workers", "worker-1");
|
|
5392
|
+
await mkdir(workerDir, { recursive: true });
|
|
5393
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5394
|
+
name: "worker-1",
|
|
5395
|
+
assigned_tasks: [],
|
|
5396
|
+
team_state_root: stateDir,
|
|
5397
|
+
});
|
|
5398
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
5399
|
+
state: "idle",
|
|
5400
|
+
updated_at: new Date().toISOString(),
|
|
5401
|
+
});
|
|
5402
|
+
|
|
5403
|
+
process.env.OMX_TEAM_WORKER = "worker-missing-assignment/worker-1";
|
|
5404
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5405
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5406
|
+
|
|
5407
|
+
const result = await dispatchCodexNativeHook(
|
|
5408
|
+
{
|
|
5409
|
+
hook_event_name: "Stop",
|
|
5410
|
+
cwd,
|
|
5411
|
+
session_id: "sess-stop-team-worker-missing-assignment",
|
|
5412
|
+
thread_id: "thread-stop-team-worker-missing-assignment",
|
|
5413
|
+
turn_id: "turn-stop-team-worker-missing-assignment",
|
|
5414
|
+
},
|
|
5415
|
+
{ cwd },
|
|
5416
|
+
);
|
|
5417
|
+
|
|
5418
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
5419
|
+
assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_task_assignment");
|
|
5420
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
|
|
5421
|
+
} finally {
|
|
5422
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5423
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
5424
|
+
if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
5425
|
+
else delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5426
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5427
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5428
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5429
|
+
}
|
|
5430
|
+
});
|
|
5431
|
+
|
|
5432
|
+
it("blocks unresolved worker Stop before generic auto-nudge can bypass it", async () => {
|
|
5433
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-state-"));
|
|
5434
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5435
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5436
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5437
|
+
const prevPath = process.env.PATH;
|
|
5438
|
+
try {
|
|
5439
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5440
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5441
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
5442
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5443
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
5444
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5445
|
+
|
|
5446
|
+
process.env.OMX_TEAM_WORKER = "worker-missing-state/worker-1";
|
|
5447
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5448
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5449
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
5450
|
+
|
|
5451
|
+
const result = await dispatchCodexNativeHook(
|
|
5452
|
+
{
|
|
5453
|
+
hook_event_name: "Stop",
|
|
5454
|
+
cwd,
|
|
5455
|
+
session_id: "sess-stop-team-worker-missing-state",
|
|
5456
|
+
thread_id: "thread-stop-team-worker-missing-state",
|
|
5457
|
+
turn_id: "turn-stop-team-worker-missing-state",
|
|
5458
|
+
last_assistant_message: "Should I proceed?",
|
|
5459
|
+
},
|
|
5460
|
+
{ cwd },
|
|
5461
|
+
);
|
|
5462
|
+
|
|
5463
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
5464
|
+
assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_worker_state");
|
|
5465
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /auto_nudge/);
|
|
5466
|
+
const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
|
|
5467
|
+
assert.doesNotMatch(tmuxLog, /native Stop allowed/);
|
|
5468
|
+
} finally {
|
|
5469
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5470
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
5471
|
+
if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
5472
|
+
else delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5473
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5474
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5475
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
5476
|
+
else delete process.env.PATH;
|
|
5477
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5478
|
+
}
|
|
5479
|
+
});
|
|
5480
|
+
|
|
5481
|
+
it("prefers canonical internal worker identity over public worker identity for Stop nudges", async () => {
|
|
5482
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-internal-env-"));
|
|
5483
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5484
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5485
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5486
|
+
const prevPath = process.env.PATH;
|
|
5487
|
+
try {
|
|
5488
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5489
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5490
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
5491
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5492
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
5493
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5494
|
+
const workerDir = join(stateDir, "team", "internal-stop-team", "workers", "worker-1");
|
|
5495
|
+
await writeJson(join(stateDir, "team", "internal-stop-team", "config.json"), {
|
|
5496
|
+
name: "internal-stop-team",
|
|
5497
|
+
tmux_session: "omx-team-worker-stop",
|
|
5498
|
+
leader_pane_id: "%42",
|
|
5499
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5500
|
+
});
|
|
5501
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5502
|
+
name: "worker-1",
|
|
5503
|
+
assigned_tasks: ["1"],
|
|
5504
|
+
team_state_root: stateDir,
|
|
5505
|
+
});
|
|
5506
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
5507
|
+
state: "done",
|
|
5508
|
+
current_task_id: "1",
|
|
5509
|
+
updated_at: new Date().toISOString(),
|
|
5510
|
+
});
|
|
5511
|
+
await writeJson(join(stateDir, "team", "internal-stop-team", "tasks", "task-1.json"), {
|
|
5512
|
+
id: "1",
|
|
5513
|
+
status: "completed",
|
|
4274
5514
|
owner: "worker-1",
|
|
4275
|
-
created_at: new Date().toISOString(),
|
|
4276
5515
|
});
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
5516
|
+
|
|
5517
|
+
process.env.OMX_TEAM_WORKER = "public-stop-team/worker-1";
|
|
5518
|
+
process.env.OMX_TEAM_INTERNAL_WORKER = "internal-stop-team/worker-1";
|
|
5519
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5520
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
5521
|
+
|
|
5522
|
+
const result = await dispatchCodexNativeHook(
|
|
5523
|
+
{
|
|
5524
|
+
hook_event_name: "Stop",
|
|
5525
|
+
cwd,
|
|
5526
|
+
session_id: "sess-stop-team-worker-internal-env",
|
|
5527
|
+
thread_id: "thread-stop-team-worker-internal-env",
|
|
5528
|
+
turn_id: "turn-stop-team-worker-internal-env",
|
|
5529
|
+
},
|
|
5530
|
+
{ cwd },
|
|
4280
5531
|
);
|
|
4281
5532
|
|
|
4282
|
-
assert.
|
|
4283
|
-
|
|
4284
|
-
assert.
|
|
4285
|
-
assert.
|
|
4286
|
-
decision: "block",
|
|
4287
|
-
reason:
|
|
4288
|
-
"OMX team worker worker-1 is still assigned non-terminal task 1 (blocked); continue the current assigned task or report a concrete blocker before stopping.",
|
|
4289
|
-
stopReason: "team_worker_worker-1_1_blocked",
|
|
4290
|
-
systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
|
|
4291
|
-
});
|
|
5533
|
+
assert.equal(result.outputJson, null);
|
|
5534
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
5535
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
5536
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), true);
|
|
4292
5537
|
} finally {
|
|
5538
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5539
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
5540
|
+
if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
5541
|
+
else delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5542
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5543
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5544
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
5545
|
+
else delete process.env.PATH;
|
|
4293
5546
|
await rm(cwd, { recursive: true, force: true });
|
|
4294
5547
|
}
|
|
4295
5548
|
});
|
|
4296
5549
|
|
|
4297
|
-
it("
|
|
4298
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-
|
|
5550
|
+
it("blocks worker Stop when canonical task ownership has a newer non-terminal task", async () => {
|
|
5551
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-owned-task-"));
|
|
4299
5552
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5553
|
+
const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4300
5554
|
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5555
|
+
const prevPath = process.env.PATH;
|
|
4301
5556
|
try {
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
5557
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5558
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5559
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
5560
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5561
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
5562
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5563
|
+
const workerDir = join(stateDir, "team", "worker-owned-task", "workers", "worker-1");
|
|
5564
|
+
await writeJson(join(stateDir, "team", "worker-owned-task", "config.json"), {
|
|
5565
|
+
name: "worker-owned-task",
|
|
5566
|
+
tmux_session: "omx-team-worker-stop",
|
|
5567
|
+
leader_pane_id: "%42",
|
|
5568
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5569
|
+
});
|
|
5570
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5571
|
+
name: "worker-1",
|
|
5572
|
+
assigned_tasks: ["1"],
|
|
5573
|
+
team_state_root: stateDir,
|
|
5574
|
+
});
|
|
4312
5575
|
await writeJson(join(workerDir, "status.json"), {
|
|
4313
5576
|
state: "done",
|
|
4314
5577
|
current_task_id: "1",
|
|
4315
5578
|
updated_at: new Date().toISOString(),
|
|
4316
5579
|
});
|
|
4317
|
-
await writeJson(join(
|
|
5580
|
+
await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-1.json"), {
|
|
4318
5581
|
id: "1",
|
|
4319
|
-
subject: "hook task",
|
|
4320
|
-
description: "finish hook task",
|
|
4321
5582
|
status: "completed",
|
|
4322
5583
|
owner: "worker-1",
|
|
4323
|
-
|
|
5584
|
+
});
|
|
5585
|
+
await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-2.json"), {
|
|
5586
|
+
id: "2",
|
|
5587
|
+
status: "in_progress",
|
|
5588
|
+
owner: "worker-1",
|
|
4324
5589
|
});
|
|
4325
5590
|
|
|
4326
|
-
process.env.OMX_TEAM_WORKER = "worker-
|
|
4327
|
-
process.env.
|
|
5591
|
+
process.env.OMX_TEAM_WORKER = "worker-owned-task/worker-1";
|
|
5592
|
+
delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
5593
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5594
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
4328
5595
|
|
|
4329
5596
|
const result = await dispatchCodexNativeHook(
|
|
4330
5597
|
{
|
|
4331
5598
|
hook_event_name: "Stop",
|
|
4332
5599
|
cwd,
|
|
4333
|
-
session_id: "sess-stop-team-worker-
|
|
5600
|
+
session_id: "sess-stop-team-worker-owned-task",
|
|
5601
|
+
thread_id: "thread-stop-team-worker-owned-task",
|
|
5602
|
+
turn_id: "turn-stop-team-worker-owned-task",
|
|
4334
5603
|
},
|
|
4335
5604
|
{ cwd },
|
|
4336
5605
|
);
|
|
4337
5606
|
|
|
4338
|
-
assert.
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
4344
|
-
});
|
|
5607
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
5608
|
+
assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_2_in_progress");
|
|
5609
|
+
assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
|
|
5610
|
+
const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
|
|
5611
|
+
assert.doesNotMatch(tmuxLog, /native Stop allowed/);
|
|
4345
5612
|
} finally {
|
|
4346
5613
|
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
4347
5614
|
else delete process.env.OMX_TEAM_WORKER;
|
|
5615
|
+
if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
|
|
5616
|
+
else delete process.env.OMX_TEAM_INTERNAL_WORKER;
|
|
4348
5617
|
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
4349
5618
|
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5619
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
5620
|
+
else delete process.env.PATH;
|
|
4350
5621
|
await rm(cwd, { recursive: true, force: true });
|
|
4351
5622
|
}
|
|
4352
5623
|
});
|
|
@@ -4655,7 +5926,6 @@ esac
|
|
|
4655
5926
|
}
|
|
4656
5927
|
});
|
|
4657
5928
|
|
|
4658
|
-
|
|
4659
5929
|
it("reads canonical Stop fallback team state from OMX_TEAM_STATE_ROOT when configured", async () => {
|
|
4660
5930
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-root-"));
|
|
4661
5931
|
const sharedRoot = join(cwd, "shared-root");
|
|
@@ -4808,13 +6078,12 @@ esac
|
|
|
4808
6078
|
);
|
|
4809
6079
|
|
|
4810
6080
|
assert.equal(result.omxEventName, "stop");
|
|
4811
|
-
assert.
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
});
|
|
6081
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6082
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
6083
|
+
assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
|
|
6084
|
+
assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
6085
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
6086
|
+
assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
|
|
4818
6087
|
} finally {
|
|
4819
6088
|
await rm(cwd, { recursive: true, force: true });
|
|
4820
6089
|
}
|
|
@@ -4949,7 +6218,7 @@ esac
|
|
|
4949
6218
|
}
|
|
4950
6219
|
});
|
|
4951
6220
|
|
|
4952
|
-
it("
|
|
6221
|
+
it("returns an explicit ralplan waiting status while subagents are still active", async () => {
|
|
4953
6222
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
|
|
4954
6223
|
try {
|
|
4955
6224
|
const stateDir = join(cwd, ".omx", "state");
|
|
@@ -5001,7 +6270,11 @@ esac
|
|
|
5001
6270
|
);
|
|
5002
6271
|
|
|
5003
6272
|
assert.equal(result.omxEventName, "stop");
|
|
5004
|
-
assert.equal(result.outputJson,
|
|
6273
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6274
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Status: waiting/);
|
|
6275
|
+
assert.match(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
|
|
6276
|
+
assert.match(String(result.outputJson?.reason ?? ""), /then continue from the current ralplan artifact/i);
|
|
6277
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_waiting_subagent");
|
|
5005
6278
|
} finally {
|
|
5006
6279
|
await rm(cwd, { recursive: true, force: true });
|
|
5007
6280
|
}
|
|
@@ -5140,6 +6413,36 @@ esac
|
|
|
5140
6413
|
}
|
|
5141
6414
|
});
|
|
5142
6415
|
|
|
6416
|
+
it("does not block Stop from stale root autoresearch state when the explicit session directory is missing", async () => {
|
|
6417
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-missing-session-autoresearch-"));
|
|
6418
|
+
try {
|
|
6419
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6420
|
+
await mkdir(stateDir, { recursive: true });
|
|
6421
|
+
await writeJson(join(stateDir, "autoresearch-state.json"), {
|
|
6422
|
+
active: true,
|
|
6423
|
+
mode: "autoresearch",
|
|
6424
|
+
current_phase: "executing",
|
|
6425
|
+
validation_mode: "mission-validator-script",
|
|
6426
|
+
mission_validator_command: "node scripts/validate.js",
|
|
6427
|
+
completion_artifact_path: ".omx/specs/autoresearch-demo/completion.json",
|
|
6428
|
+
});
|
|
6429
|
+
|
|
6430
|
+
const result = await dispatchCodexNativeHook(
|
|
6431
|
+
{
|
|
6432
|
+
hook_event_name: "Stop",
|
|
6433
|
+
cwd,
|
|
6434
|
+
session_id: "missing-session",
|
|
6435
|
+
},
|
|
6436
|
+
{ cwd },
|
|
6437
|
+
);
|
|
6438
|
+
|
|
6439
|
+
assert.equal(result.omxEventName, "stop");
|
|
6440
|
+
assert.equal(result.outputJson, null);
|
|
6441
|
+
} finally {
|
|
6442
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6443
|
+
}
|
|
6444
|
+
});
|
|
6445
|
+
|
|
5143
6446
|
it("does not block Stop solely because deep-interview is active", async () => {
|
|
5144
6447
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
|
|
5145
6448
|
try {
|
|
@@ -5999,6 +7302,51 @@ esac
|
|
|
5999
7302
|
}
|
|
6000
7303
|
});
|
|
6001
7304
|
|
|
7305
|
+
it("does not block Stop when Ralph skill-active initialization points at another session", async () => {
|
|
7306
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-stale-skill-active-"));
|
|
7307
|
+
try {
|
|
7308
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7309
|
+
const currentSessionId = "sess-current-ralph";
|
|
7310
|
+
await mkdir(join(stateDir, "sessions", currentSessionId), { recursive: true });
|
|
7311
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
7312
|
+
session_id: currentSessionId,
|
|
7313
|
+
native_session_id: currentSessionId,
|
|
7314
|
+
cwd,
|
|
7315
|
+
});
|
|
7316
|
+
await writeJson(join(stateDir, "sessions", currentSessionId, "ralph-state.json"), {
|
|
7317
|
+
active: true,
|
|
7318
|
+
mode: "ralph",
|
|
7319
|
+
current_phase: "verifying",
|
|
7320
|
+
session_id: currentSessionId,
|
|
7321
|
+
owner_omx_session_id: currentSessionId,
|
|
7322
|
+
task_slug: "stale-rebound-task",
|
|
7323
|
+
});
|
|
7324
|
+
await writeJson(join(stateDir, "sessions", currentSessionId, "skill-active-state.json"), {
|
|
7325
|
+
active: true,
|
|
7326
|
+
skill: "ralph",
|
|
7327
|
+
phase: "verifying",
|
|
7328
|
+
session_id: currentSessionId,
|
|
7329
|
+
initialized_mode: "ralph",
|
|
7330
|
+
initialized_state_path: ".omx/state/sessions/sess-old-ralph/ralph-state.json",
|
|
7331
|
+
active_skills: [{ skill: "ralph", phase: "verifying", active: true, session_id: currentSessionId }],
|
|
7332
|
+
});
|
|
7333
|
+
|
|
7334
|
+
const result = await dispatchCodexNativeHook(
|
|
7335
|
+
{
|
|
7336
|
+
hook_event_name: "Stop",
|
|
7337
|
+
cwd,
|
|
7338
|
+
session_id: currentSessionId,
|
|
7339
|
+
},
|
|
7340
|
+
{ cwd },
|
|
7341
|
+
);
|
|
7342
|
+
|
|
7343
|
+
assert.equal(result.omxEventName, "stop");
|
|
7344
|
+
assert.equal(result.outputJson, null);
|
|
7345
|
+
} finally {
|
|
7346
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7347
|
+
}
|
|
7348
|
+
});
|
|
7349
|
+
|
|
6002
7350
|
it("blocks same-session Ralph Stop continuation when ownership identifiers match", async () => {
|
|
6003
7351
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-owned-session-"));
|
|
6004
7352
|
const previousTmuxPane = process.env.TMUX_PANE;
|
|
@@ -6050,6 +7398,98 @@ esac
|
|
|
6050
7398
|
}
|
|
6051
7399
|
});
|
|
6052
7400
|
|
|
7401
|
+
it("allows native verifier subagent Stop to complete while leader Ralph remains active", async () => {
|
|
7402
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-subagent-verdict-"));
|
|
7403
|
+
try {
|
|
7404
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7405
|
+
const omxSessionId = "sess-ralph-leader-verifier";
|
|
7406
|
+
const leaderNativeSessionId = "codex-ralph-leader-verifier";
|
|
7407
|
+
const childNativeSessionId = "codex-verifier-child";
|
|
7408
|
+
await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
|
|
7409
|
+
await writeSessionStart(cwd, omxSessionId, {
|
|
7410
|
+
nativeSessionId: leaderNativeSessionId,
|
|
7411
|
+
});
|
|
7412
|
+
await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
|
|
7413
|
+
active: true,
|
|
7414
|
+
mode: "ralph",
|
|
7415
|
+
current_phase: "verifying",
|
|
7416
|
+
session_id: omxSessionId,
|
|
7417
|
+
owner_omx_session_id: omxSessionId,
|
|
7418
|
+
owner_codex_session_id: leaderNativeSessionId,
|
|
7419
|
+
});
|
|
7420
|
+
|
|
7421
|
+
const transcriptPath = join(cwd, "verifier-subagent-rollout.jsonl");
|
|
7422
|
+
await writeFile(
|
|
7423
|
+
transcriptPath,
|
|
7424
|
+
`${JSON.stringify({
|
|
7425
|
+
type: "session_meta",
|
|
7426
|
+
payload: {
|
|
7427
|
+
id: childNativeSessionId,
|
|
7428
|
+
source: {
|
|
7429
|
+
subagent: {
|
|
7430
|
+
thread_spawn: {
|
|
7431
|
+
parent_thread_id: leaderNativeSessionId,
|
|
7432
|
+
depth: 1,
|
|
7433
|
+
agent_nickname: "Verifier",
|
|
7434
|
+
agent_role: "verifier",
|
|
7435
|
+
},
|
|
7436
|
+
},
|
|
7437
|
+
},
|
|
7438
|
+
agent_nickname: "Verifier",
|
|
7439
|
+
agent_role: "verifier",
|
|
7440
|
+
},
|
|
7441
|
+
})}\n`,
|
|
7442
|
+
);
|
|
7443
|
+
|
|
7444
|
+
await dispatchCodexNativeHook(
|
|
7445
|
+
{
|
|
7446
|
+
hook_event_name: "SessionStart",
|
|
7447
|
+
cwd,
|
|
7448
|
+
session_id: childNativeSessionId,
|
|
7449
|
+
transcript_path: transcriptPath,
|
|
7450
|
+
},
|
|
7451
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
7452
|
+
);
|
|
7453
|
+
|
|
7454
|
+
const childStop = await dispatchCodexNativeHook(
|
|
7455
|
+
{
|
|
7456
|
+
hook_event_name: "Stop",
|
|
7457
|
+
cwd,
|
|
7458
|
+
session_id: childNativeSessionId,
|
|
7459
|
+
thread_id: childNativeSessionId,
|
|
7460
|
+
last_assistant_message: "Verdict: APPROVED. Evidence is sufficient.",
|
|
7461
|
+
},
|
|
7462
|
+
{ cwd },
|
|
7463
|
+
);
|
|
7464
|
+
|
|
7465
|
+
assert.equal(childStop.omxEventName, "stop");
|
|
7466
|
+
assert.equal(childStop.outputJson, null);
|
|
7467
|
+
|
|
7468
|
+
const leaderStop = await dispatchCodexNativeHook(
|
|
7469
|
+
{
|
|
7470
|
+
hook_event_name: "Stop",
|
|
7471
|
+
cwd,
|
|
7472
|
+
session_id: leaderNativeSessionId,
|
|
7473
|
+
thread_id: leaderNativeSessionId,
|
|
7474
|
+
last_assistant_message: "Waiting on verification integration.",
|
|
7475
|
+
},
|
|
7476
|
+
{ cwd },
|
|
7477
|
+
);
|
|
7478
|
+
|
|
7479
|
+
assert.equal(leaderStop.omxEventName, "stop");
|
|
7480
|
+
assert.deepEqual(leaderStop.outputJson, {
|
|
7481
|
+
decision: "block",
|
|
7482
|
+
reason:
|
|
7483
|
+
"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.",
|
|
7484
|
+
stopReason: "ralph_verifying",
|
|
7485
|
+
systemMessage:
|
|
7486
|
+
"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.",
|
|
7487
|
+
});
|
|
7488
|
+
} finally {
|
|
7489
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7490
|
+
}
|
|
7491
|
+
});
|
|
7492
|
+
|
|
6053
7493
|
it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
|
|
6054
7494
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
|
|
6055
7495
|
try {
|
|
@@ -6340,7 +7780,6 @@ esac
|
|
|
6340
7780
|
}
|
|
6341
7781
|
});
|
|
6342
7782
|
|
|
6343
|
-
|
|
6344
7783
|
it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
|
|
6345
7784
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
6346
7785
|
try {
|
|
@@ -6720,12 +8159,11 @@ esac
|
|
|
6720
8159
|
}
|
|
6721
8160
|
});
|
|
6722
8161
|
|
|
6723
|
-
it("suppresses native auto-nudge when root deep-interview mode state is active
|
|
8162
|
+
it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
|
|
6724
8163
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
|
|
6725
8164
|
try {
|
|
6726
8165
|
const stateDir = join(cwd, ".omx", "state");
|
|
6727
8166
|
await mkdir(stateDir, { recursive: true });
|
|
6728
|
-
process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
|
|
6729
8167
|
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
6730
8168
|
active: true,
|
|
6731
8169
|
mode: "deep-interview",
|
|
@@ -6749,6 +8187,68 @@ esac
|
|
|
6749
8187
|
}
|
|
6750
8188
|
});
|
|
6751
8189
|
|
|
8190
|
+
it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
|
|
8191
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
|
|
8192
|
+
try {
|
|
8193
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8194
|
+
await mkdir(stateDir, { recursive: true });
|
|
8195
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
|
|
8196
|
+
|
|
8197
|
+
const result = await dispatchCodexNativeHook(
|
|
8198
|
+
{
|
|
8199
|
+
hook_event_name: "Stop",
|
|
8200
|
+
cwd,
|
|
8201
|
+
thread_id: "thread-stop-auto-env-session",
|
|
8202
|
+
turn_id: "turn-stop-auto-env-session-1",
|
|
8203
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
8204
|
+
},
|
|
8205
|
+
{ cwd },
|
|
8206
|
+
);
|
|
8207
|
+
|
|
8208
|
+
assert.equal(result.omxEventName, "stop");
|
|
8209
|
+
assert.deepEqual(result.outputJson, {
|
|
8210
|
+
decision: "block",
|
|
8211
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
8212
|
+
stopReason: "auto_nudge",
|
|
8213
|
+
systemMessage:
|
|
8214
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
8215
|
+
});
|
|
8216
|
+
const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
|
|
8217
|
+
assert.ok((stopState.sessions as Record<string, unknown>)["sess-stop-auto-mode"]);
|
|
8218
|
+
} finally {
|
|
8219
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8220
|
+
}
|
|
8221
|
+
});
|
|
8222
|
+
|
|
8223
|
+
|
|
8224
|
+
it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
|
|
8225
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
|
|
8226
|
+
try {
|
|
8227
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8228
|
+
await mkdir(stateDir, { recursive: true });
|
|
8229
|
+
process.env.SESSION_ID = "generic-shell-session";
|
|
8230
|
+
|
|
8231
|
+
const result = await dispatchCodexNativeHook(
|
|
8232
|
+
{
|
|
8233
|
+
hook_event_name: "Stop",
|
|
8234
|
+
cwd,
|
|
8235
|
+
thread_id: "thread-stop-auto-generic-session",
|
|
8236
|
+
turn_id: "turn-stop-auto-generic-session-1",
|
|
8237
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
8238
|
+
},
|
|
8239
|
+
{ cwd },
|
|
8240
|
+
);
|
|
8241
|
+
|
|
8242
|
+
assert.equal(result.omxEventName, "stop");
|
|
8243
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
8244
|
+
const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
|
|
8245
|
+
const sessions = stopState.sessions as Record<string, unknown>;
|
|
8246
|
+
assert.equal(sessions["generic-shell-session"], undefined);
|
|
8247
|
+
assert.ok(sessions["thread-stop-auto-generic-session"]);
|
|
8248
|
+
} finally {
|
|
8249
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8250
|
+
}
|
|
8251
|
+
});
|
|
6752
8252
|
it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
|
|
6753
8253
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
|
|
6754
8254
|
try {
|
|
@@ -7093,8 +8593,8 @@ esac
|
|
|
7093
8593
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
|
|
7094
8594
|
try {
|
|
7095
8595
|
const stateDir = join(cwd, ".omx", "state");
|
|
7096
|
-
await mkdir(stateDir, { recursive: true });
|
|
7097
|
-
await writeJson(join(stateDir, "ultrawork-state.json"), {
|
|
8596
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
|
|
8597
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
|
|
7098
8598
|
active: true,
|
|
7099
8599
|
current_phase: "executing",
|
|
7100
8600
|
});
|
|
@@ -7188,13 +8688,10 @@ esac
|
|
|
7188
8688
|
);
|
|
7189
8689
|
|
|
7190
8690
|
assert.equal(repeated.omxEventName, "stop");
|
|
7191
|
-
assert.
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
stopReason: "skill_ralplan_planning",
|
|
7196
|
-
systemMessage: "OMX skill ralplan is still active (phase: planning).",
|
|
7197
|
-
});
|
|
8691
|
+
assert.equal(repeated.outputJson?.decision, "block");
|
|
8692
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
8693
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
8694
|
+
assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
7198
8695
|
} finally {
|
|
7199
8696
|
await rm(cwd, { recursive: true, force: true });
|
|
7200
8697
|
}
|