oh-my-codex 0.11.13 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/README.md +34 -17
- package/crates/omx-runtime/src/main.rs +6 -2
- package/dist/agents/native-config.js +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js +74 -2
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/cleanup.test.js +37 -30
- package/dist/cli/__tests__/cleanup.test.js.map +1 -1
- package/dist/cli/__tests__/error-handling-warnings.test.js +3 -1
- package/dist/cli/__tests__/error-handling-warnings.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +276 -5
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +95 -1
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +49 -9
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +9 -0
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +136 -11
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +10 -0
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -0
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/autoresearch-guided.d.ts.map +1 -1
- package/dist/cli/autoresearch-guided.js +2 -1
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch.d.ts.map +1 -1
- package/dist/cli/autoresearch.js +2 -1
- package/dist/cli/autoresearch.js.map +1 -1
- package/dist/cli/cleanup.d.ts.map +1 -1
- package/dist/cli/cleanup.js +10 -5
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/index.d.ts +21 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +298 -36
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/omx.js +2 -0
- package/dist/cli/omx.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +41 -7
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +16 -557
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +34 -9
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +79 -2
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +2 -0
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +11 -0
- package/dist/config/codex-hooks.d.ts.map +1 -0
- package/dist/config/codex-hooks.js +50 -0
- package/dist/config/codex-hooks.js.map +1 -0
- package/dist/config/generator.d.ts +5 -3
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +24 -14
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +20 -0
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +132 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +292 -4
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +86 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +40 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +54 -0
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-modules.test.js +31 -0
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +51 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +136 -0
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +120 -0
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +145 -20
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +116 -0
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +86 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js +1 -0
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/runtime.test.js +49 -0
- package/dist/hooks/extensibility/__tests__/runtime.test.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js +10 -0
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +1 -1
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +2 -0
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +76 -4
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +12 -8
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/session.d.ts +5 -1
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +10 -6
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +6 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +0 -3
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +27 -1
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +0 -5
- package/dist/mcp/__tests__/server-lifecycle.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 +0 -1
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +20 -0
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +6 -5
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/notifications/__tests__/idle-cooldown.test.js +24 -1
- package/dist/notifications/__tests__/idle-cooldown.test.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js +20 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/__tests__/tmux.test.js +41 -0
- package/dist/notifications/__tests__/tmux.test.js.map +1 -1
- package/dist/notifications/idle-cooldown.d.ts +13 -0
- package/dist/notifications/idle-cooldown.d.ts.map +1 -1
- package/dist/notifications/idle-cooldown.js +50 -16
- package/dist/notifications/idle-cooldown.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js +2 -0
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/tmux.d.ts.map +1 -1
- package/dist/notifications/tmux.js +4 -0
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.d.ts +2 -0
- package/dist/scripts/__tests__/codex-native-hook.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/codex-native-hook.test.js +1050 -0
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +22 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -0
- package/dist/scripts/codex-native-hook.js +792 -0
- package/dist/scripts/codex-native-hook.js.map +1 -0
- package/dist/scripts/codex-native-pre-post.d.ts +26 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -0
- package/dist/scripts/codex-native-pre-post.js +118 -0
- package/dist/scripts/codex-native-pre-post.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +322 -21
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +5 -6
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/log.d.ts +2 -2
- package/dist/scripts/notify-hook/log.d.ts.map +1 -1
- package/dist/scripts/notify-hook/log.js +10 -2
- package/dist/scripts/notify-hook/log.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +2 -0
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +18 -0
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -0
- package/dist/scripts/notify-hook/orchestration-intent.js +72 -0
- package/dist/scripts/notify-hook/orchestration-intent.js.map +1 -0
- package/dist/scripts/notify-hook/process-runner.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +7 -0
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +15 -6
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +125 -6
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +3 -2
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +165 -37
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +33 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +68 -5
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/utils.d.ts +1 -1
- package/dist/scripts/notify-hook/utils.d.ts.map +1 -1
- package/dist/scripts/notify-hook/utils.js.map +1 -1
- package/dist/scripts/notify-hook.js +55 -32
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +344 -18
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts +2 -0
- package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts.map +1 -0
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +671 -0
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -0
- package/dist/team/__tests__/mcp-comm.test.js +5 -0
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +543 -15
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +133 -8
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +4 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +160 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +19 -1
- 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 +95 -23
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/contracts.d.ts +11 -1
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/contracts.js +29 -0
- package/dist/team/contracts.js.map +1 -1
- package/dist/team/delivery-log.d.ts +14 -0
- package/dist/team/delivery-log.d.ts.map +1 -0
- package/dist/team/delivery-log.js +35 -0
- package/dist/team/delivery-log.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +2 -2
- package/dist/team/idle-nudge.js +2 -2
- package/dist/team/mcp-comm.d.ts +4 -0
- package/dist/team/mcp-comm.d.ts.map +1 -1
- package/dist/team/mcp-comm.js +84 -1
- package/dist/team/mcp-comm.js.map +1 -1
- package/dist/team/pane-status.d.ts +149 -0
- package/dist/team/pane-status.d.ts.map +1 -0
- package/dist/team/pane-status.js +558 -0
- package/dist/team/pane-status.js.map +1 -0
- package/dist/team/reminder-intents.d.ts +11 -0
- package/dist/team/reminder-intents.d.ts.map +1 -0
- package/dist/team/reminder-intents.js +40 -0
- package/dist/team/reminder-intents.js.map +1 -0
- package/dist/team/runtime-cli.d.ts +1 -1
- package/dist/team/runtime-cli.js +2 -2
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +409 -191
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +6 -5
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts +4 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +59 -18
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/mailbox.d.ts.map +1 -1
- package/dist/team/state/mailbox.js +45 -2
- package/dist/team/state/mailbox.js.map +1 -1
- package/dist/team/state/monitor.d.ts +2 -1
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +30 -1
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/types.d.ts +5 -2
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state.d.ts +30 -3
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +170 -2
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -1
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +2 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +19 -3
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +4 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +33 -6
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +63 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/__tests__/platform-command.test.js +50 -4
- package/dist/utils/__tests__/platform-command.test.js.map +1 -1
- package/dist/utils/paths.d.ts +12 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +44 -2
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.d.ts.map +1 -1
- package/dist/utils/platform-command.js +13 -5
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +10 -1
- package/dist/utils/sleep.js.map +1 -1
- package/package.json +1 -1
- package/prompts/analyst.md +2 -2
- package/prompts/api-reviewer.md +2 -2
- package/prompts/architect.md +2 -2
- package/prompts/build-fixer.md +2 -2
- package/prompts/code-reviewer.md +2 -2
- package/prompts/code-simplifier.md +1 -1
- package/prompts/critic.md +2 -2
- package/prompts/debugger.md +3 -2
- package/prompts/dependency-expert.md +2 -2
- package/prompts/designer.md +2 -2
- package/prompts/executor.md +3 -2
- package/prompts/explore.md +2 -2
- package/prompts/git-master.md +2 -2
- package/prompts/information-architect.md +15 -102
- package/prompts/performance-reviewer.md +2 -2
- package/prompts/planner.md +3 -2
- package/prompts/product-analyst.md +2 -2
- package/prompts/product-manager.md +2 -2
- package/prompts/qa-tester.md +2 -2
- package/prompts/quality-reviewer.md +2 -2
- package/prompts/quality-strategist.md +2 -2
- package/prompts/researcher.md +2 -2
- package/prompts/security-reviewer.md +2 -2
- package/prompts/sisyphus-lite.md +2 -2
- package/prompts/style-reviewer.md +2 -2
- package/prompts/team-executor.md +2 -2
- package/prompts/test-engineer.md +2 -2
- package/prompts/ux-researcher.md +2 -2
- package/prompts/verifier.md +3 -2
- package/prompts/vision.md +2 -2
- package/prompts/writer.md +2 -2
- package/skills/team/SKILL.md +18 -33
- package/src/scripts/__tests__/codex-native-hook.test.ts +1346 -0
- package/src/scripts/codex-native-hook.ts +983 -0
- package/src/scripts/codex-native-pre-post.ts +161 -0
- package/src/scripts/notify-fallback-watcher.ts +378 -29
- package/src/scripts/notify-hook/auto-nudge.ts +5 -10
- package/src/scripts/notify-hook/log.ts +18 -4
- package/src/scripts/notify-hook/managed-tmux.ts +1 -0
- package/src/scripts/notify-hook/orchestration-intent.ts +82 -0
- package/src/scripts/notify-hook/process-runner.ts +4 -4
- package/src/scripts/notify-hook/ralph-session-resume.ts +9 -0
- package/src/scripts/notify-hook/team-dispatch.ts +134 -6
- package/src/scripts/notify-hook/team-leader-nudge.ts +183 -37
- package/src/scripts/notify-hook/team-tmux-guard.ts +35 -43
- package/src/scripts/notify-hook/team-worker.ts +73 -4
- package/src/scripts/notify-hook/utils.ts +1 -1
- package/src/scripts/notify-hook.ts +64 -32
- package/templates/AGENTS.md +21 -11
- package/README.de.md +0 -263
- package/README.el.md +0 -223
- package/README.es.md +0 -263
- package/README.fr.md +0 -263
- package/README.it.md +0 -263
- package/README.ja.md +0 -264
- package/README.ko.md +0 -264
- package/README.pl.md +0 -216
- package/README.pt.md +0 -263
- package/README.ru.md +0 -263
- package/README.tr.md +0 -263
- package/README.vi.md +0 -223
- package/README.zh-TW.md +0 -293
- package/README.zh.md +0 -264
- package/dist/mcp/__tests__/team-server-cleanup.test.d.ts +0 -2
- package/dist/mcp/__tests__/team-server-cleanup.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/team-server-cleanup.test.js +0 -219
- package/dist/mcp/__tests__/team-server-cleanup.test.js.map +0 -1
- package/dist/mcp/__tests__/team-server-runtime-deps.test.d.ts +0 -2
- package/dist/mcp/__tests__/team-server-runtime-deps.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/team-server-runtime-deps.test.js +0 -13
- package/dist/mcp/__tests__/team-server-runtime-deps.test.js.map +0 -1
- package/dist/mcp/__tests__/team-server-wait.test.d.ts +0 -2
- package/dist/mcp/__tests__/team-server-wait.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/team-server-wait.test.js +0 -155
- package/dist/mcp/__tests__/team-server-wait.test.js.map +0 -1
- package/dist/mcp/team-server.d.ts +0 -24
- package/dist/mcp/team-server.d.ts.map +0 -1
- package/dist/mcp/team-server.js +0 -482
- package/dist/mcp/team-server.js.map +0 -1
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
import { readModeState } from "../modes/base.js";
|
|
6
|
+
import { getReadScopedStateDirs } from "../mcp/state-paths.js";
|
|
7
|
+
import { readSubagentSessionSummary } from "../subagents/tracker.js";
|
|
8
|
+
import { resolveCanonicalTeamStateRoot } from "../team/state-root.js";
|
|
9
|
+
import { readTeamManifestV2, readTeamPhase } from "../team/state.js";
|
|
10
|
+
import { omxNotepadPath, omxProjectMemoryPath } from "../utils/paths.js";
|
|
11
|
+
import {
|
|
12
|
+
detectPrimaryKeyword,
|
|
13
|
+
recordSkillActivation,
|
|
14
|
+
type SkillActiveState,
|
|
15
|
+
} from "../hooks/keyword-detector.js";
|
|
16
|
+
import {
|
|
17
|
+
detectStallPattern,
|
|
18
|
+
isDeepInterviewInputLockActive,
|
|
19
|
+
isDeepInterviewStateActive,
|
|
20
|
+
loadAutoNudgeConfig,
|
|
21
|
+
normalizeAutoNudgeSignatureText,
|
|
22
|
+
} from "./notify-hook/auto-nudge.js";
|
|
23
|
+
import {
|
|
24
|
+
buildNativePostToolUseOutput,
|
|
25
|
+
buildNativePreToolUseOutput,
|
|
26
|
+
} from "./codex-native-pre-post.js";
|
|
27
|
+
import {
|
|
28
|
+
buildNativeHookEvent,
|
|
29
|
+
} from "../hooks/extensibility/events.js";
|
|
30
|
+
import type { HookEventEnvelope } from "../hooks/extensibility/types.js";
|
|
31
|
+
import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
|
|
32
|
+
import { writeSessionStart } from "../hooks/session.js";
|
|
33
|
+
|
|
34
|
+
type CodexHookEventName =
|
|
35
|
+
| "SessionStart"
|
|
36
|
+
| "PreToolUse"
|
|
37
|
+
| "PostToolUse"
|
|
38
|
+
| "UserPromptSubmit"
|
|
39
|
+
| "Stop";
|
|
40
|
+
|
|
41
|
+
type CodexHookPayload = Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
interface NativeHookDispatchOptions {
|
|
44
|
+
cwd?: string;
|
|
45
|
+
sessionOwnerPid?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NativeHookDispatchResult {
|
|
49
|
+
hookEventName: CodexHookEventName | null;
|
|
50
|
+
omxEventName: string | null;
|
|
51
|
+
skillState: SkillActiveState | null;
|
|
52
|
+
outputJson: Record<string, unknown> | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const TERMINAL_RALPH_PHASES = new Set(["complete", "failed", "cancelled"]);
|
|
56
|
+
const TERMINAL_MODE_PHASES = new Set(["complete", "failed", "cancelled"]);
|
|
57
|
+
const SKILL_STOP_BLOCKERS = new Set(["ralplan", "deep-interview"]);
|
|
58
|
+
const TEAM_TERMINAL_TASK_STATUSES = new Set(["completed", "failed"]);
|
|
59
|
+
const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
|
|
60
|
+
|
|
61
|
+
function safeString(value: unknown): string {
|
|
62
|
+
return typeof value === "string" ? value : "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeObject(value: unknown): Record<string, unknown> {
|
|
66
|
+
return value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safePositiveInteger(value: unknown): number | null {
|
|
70
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
71
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
72
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
73
|
+
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readHookEventName(payload: CodexHookPayload): CodexHookEventName | null {
|
|
79
|
+
const raw = safeString(
|
|
80
|
+
payload.hook_event_name
|
|
81
|
+
?? payload.hookEventName
|
|
82
|
+
?? payload.event
|
|
83
|
+
?? payload.name,
|
|
84
|
+
).trim();
|
|
85
|
+
if (
|
|
86
|
+
raw === "SessionStart"
|
|
87
|
+
|| raw === "PreToolUse"
|
|
88
|
+
|| raw === "PostToolUse"
|
|
89
|
+
|| raw === "UserPromptSubmit"
|
|
90
|
+
|| raw === "Stop"
|
|
91
|
+
) {
|
|
92
|
+
return raw;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function mapCodexHookEventToOmxEvent(
|
|
98
|
+
hookEventName: CodexHookEventName | null,
|
|
99
|
+
): string | null {
|
|
100
|
+
switch (hookEventName) {
|
|
101
|
+
case "SessionStart":
|
|
102
|
+
return "session-start";
|
|
103
|
+
case "PreToolUse":
|
|
104
|
+
return "pre-tool-use";
|
|
105
|
+
case "PostToolUse":
|
|
106
|
+
return "post-tool-use";
|
|
107
|
+
case "UserPromptSubmit":
|
|
108
|
+
return "keyword-detector";
|
|
109
|
+
case "Stop":
|
|
110
|
+
return "stop";
|
|
111
|
+
default:
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readPromptText(payload: CodexHookPayload): string {
|
|
117
|
+
const candidates = [
|
|
118
|
+
payload.prompt,
|
|
119
|
+
payload.input,
|
|
120
|
+
payload.user_prompt,
|
|
121
|
+
payload.userPrompt,
|
|
122
|
+
payload.text,
|
|
123
|
+
];
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
const value = safeString(candidate).trim();
|
|
126
|
+
if (value) return value;
|
|
127
|
+
}
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildBaseContext(
|
|
132
|
+
cwd: string,
|
|
133
|
+
payload: CodexHookPayload,
|
|
134
|
+
hookEventName: CodexHookEventName,
|
|
135
|
+
): Record<string, unknown> {
|
|
136
|
+
return {
|
|
137
|
+
cwd,
|
|
138
|
+
project_path: cwd,
|
|
139
|
+
transcript_path: safeString(payload.transcript_path ?? payload.transcriptPath) || null,
|
|
140
|
+
source: safeString(payload.source),
|
|
141
|
+
payload,
|
|
142
|
+
...(hookEventName === "UserPromptSubmit"
|
|
143
|
+
? { prompt: readPromptText(payload) }
|
|
144
|
+
: {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function readJsonIfExists(path: string): Promise<Record<string, unknown> | null> {
|
|
149
|
+
if (!existsSync(path)) return null;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(await readFile(path, "utf-8")) as Record<string, unknown>;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function readScopedJsonState(
|
|
158
|
+
fileName: string,
|
|
159
|
+
cwd: string,
|
|
160
|
+
sessionId?: string,
|
|
161
|
+
): Promise<Record<string, unknown> | null> {
|
|
162
|
+
const dirs = await getReadScopedStateDirs(cwd, sessionId);
|
|
163
|
+
for (const dir of dirs) {
|
|
164
|
+
const candidate = await readJsonIfExists(join(dir, fileName));
|
|
165
|
+
if (candidate) return candidate;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isNonTerminalPhase(value: unknown): boolean {
|
|
171
|
+
const phase = safeString(value).trim().toLowerCase();
|
|
172
|
+
return phase !== "" && !TERMINAL_MODE_PHASES.has(phase);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatPhase(value: unknown, fallback = "active"): string {
|
|
176
|
+
const phase = safeString(value).trim();
|
|
177
|
+
return phase || fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readActiveRalphState(stateDir: string): Promise<Record<string, unknown> | null> {
|
|
181
|
+
const direct = await readJsonIfExists(join(stateDir, "ralph-state.json"));
|
|
182
|
+
if (direct?.active === true && !TERMINAL_RALPH_PHASES.has(safeString(direct.current_phase).trim().toLowerCase())) {
|
|
183
|
+
return direct;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const sessionInfo = await readJsonIfExists(join(stateDir, "session.json"));
|
|
187
|
+
const currentOmxSessionId = safeString(sessionInfo?.session_id).trim();
|
|
188
|
+
if (!currentOmxSessionId) return null;
|
|
189
|
+
|
|
190
|
+
const sessionScoped = await readJsonIfExists(
|
|
191
|
+
join(stateDir, "sessions", currentOmxSessionId, "ralph-state.json"),
|
|
192
|
+
);
|
|
193
|
+
if (
|
|
194
|
+
sessionScoped?.active === true
|
|
195
|
+
&& !TERMINAL_RALPH_PHASES.has(
|
|
196
|
+
safeString(sessionScoped.current_phase).trim().toLowerCase(),
|
|
197
|
+
)
|
|
198
|
+
) {
|
|
199
|
+
return sessionScoped;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sessionsRoot = join(stateDir, "sessions");
|
|
203
|
+
if (!existsSync(sessionsRoot)) return null;
|
|
204
|
+
const entries = await readdir(sessionsRoot, { withFileTypes: true }).catch(() => []);
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
if (!entry.isDirectory()) continue;
|
|
207
|
+
const candidate = await readJsonIfExists(join(sessionsRoot, entry.name, "ralph-state.json"));
|
|
208
|
+
if (
|
|
209
|
+
candidate?.active === true
|
|
210
|
+
&& !TERMINAL_RALPH_PHASES.has(
|
|
211
|
+
safeString(candidate.current_phase).trim().toLowerCase(),
|
|
212
|
+
)
|
|
213
|
+
) {
|
|
214
|
+
return candidate;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readParentPid(pid: number): number | null {
|
|
222
|
+
try {
|
|
223
|
+
if (process.platform === "linux") {
|
|
224
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
225
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
226
|
+
if (commandEnd === -1) return null;
|
|
227
|
+
const remainder = stat.slice(commandEnd + 1).trim();
|
|
228
|
+
const fields = remainder.split(/\s+/);
|
|
229
|
+
const ppid = Number(fields[1]);
|
|
230
|
+
return Number.isFinite(ppid) && ppid > 0 ? ppid : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const raw = execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
|
|
234
|
+
encoding: "utf-8",
|
|
235
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
236
|
+
}).trim();
|
|
237
|
+
const ppid = Number.parseInt(raw, 10);
|
|
238
|
+
return Number.isFinite(ppid) && ppid > 0 ? ppid : null;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readProcessCommand(pid: number): string {
|
|
245
|
+
try {
|
|
246
|
+
if (process.platform === "linux") {
|
|
247
|
+
return readFileSync(`/proc/${pid}/cmdline`, "utf-8")
|
|
248
|
+
.replace(/\u0000+/g, " ")
|
|
249
|
+
.trim();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return execFileSync("ps", ["-o", "command=", "-p", String(pid)], {
|
|
253
|
+
encoding: "utf-8",
|
|
254
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
255
|
+
}).trim();
|
|
256
|
+
} catch {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function looksLikeShellCommand(command: string): boolean {
|
|
262
|
+
return /(^|[\/\s])(bash|zsh|sh|dash|fish|ksh)(\s|$)/i.test(command);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function looksLikeCodexCommand(command: string): boolean {
|
|
266
|
+
if (/codex-native-hook(?:\.js)?/i.test(command)) return false;
|
|
267
|
+
return /\bcodex(?:\.js)?\b/i.test(command);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function resolveSessionOwnerPidFromAncestry(
|
|
271
|
+
startPid: number,
|
|
272
|
+
options: {
|
|
273
|
+
readParentPid?: (pid: number) => number | null;
|
|
274
|
+
readProcessCommand?: (pid: number) => string;
|
|
275
|
+
} = {},
|
|
276
|
+
): number | null {
|
|
277
|
+
const readParent = options.readParentPid ?? readParentPid;
|
|
278
|
+
const readCommand = options.readProcessCommand ?? readProcessCommand;
|
|
279
|
+
const lineage: Array<{ pid: number; command: string }> = [];
|
|
280
|
+
let currentPid = startPid;
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < 6 && Number.isInteger(currentPid) && currentPid > 1; i += 1) {
|
|
283
|
+
const command = readCommand(currentPid);
|
|
284
|
+
lineage.push({ pid: currentPid, command });
|
|
285
|
+
const nextPid = readParent(currentPid);
|
|
286
|
+
if (!nextPid || nextPid === currentPid) break;
|
|
287
|
+
currentPid = nextPid;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const codexAncestor = lineage.find((entry) => looksLikeCodexCommand(entry.command));
|
|
291
|
+
if (codexAncestor) return codexAncestor.pid;
|
|
292
|
+
|
|
293
|
+
if (lineage.length >= 2 && looksLikeShellCommand(lineage[0]?.command || "")) {
|
|
294
|
+
return lineage[1].pid;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (lineage.length >= 1) return lineage[0].pid;
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function resolveSessionOwnerPid(payload: CodexHookPayload): number {
|
|
302
|
+
const explicitPid = [
|
|
303
|
+
payload.session_pid,
|
|
304
|
+
payload.sessionPid,
|
|
305
|
+
payload.codex_pid,
|
|
306
|
+
payload.codexPid,
|
|
307
|
+
payload.parent_pid,
|
|
308
|
+
payload.parentPid,
|
|
309
|
+
]
|
|
310
|
+
.map(safePositiveInteger)
|
|
311
|
+
.find((value): value is number => value !== null);
|
|
312
|
+
if (explicitPid) return explicitPid;
|
|
313
|
+
|
|
314
|
+
const resolved = resolveSessionOwnerPidFromAncestry(process.ppid);
|
|
315
|
+
if (resolved) return resolved;
|
|
316
|
+
return process.pid;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function ensureOmxGitignoreEntry(cwd: string): Promise<{ changed: boolean; gitignorePath?: string }> {
|
|
320
|
+
let repoRoot = "";
|
|
321
|
+
try {
|
|
322
|
+
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
323
|
+
cwd,
|
|
324
|
+
encoding: "utf-8",
|
|
325
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
326
|
+
windowsHide: true,
|
|
327
|
+
}).trim();
|
|
328
|
+
} catch {
|
|
329
|
+
return { changed: false };
|
|
330
|
+
}
|
|
331
|
+
if (!repoRoot) return { changed: false };
|
|
332
|
+
|
|
333
|
+
const gitignorePath = join(repoRoot, ".gitignore");
|
|
334
|
+
const existing = existsSync(gitignorePath)
|
|
335
|
+
? await readFile(gitignorePath, "utf-8")
|
|
336
|
+
: "";
|
|
337
|
+
const lines = existing.split(/\r?\n/).map((line) => line.trim());
|
|
338
|
+
if (lines.includes(".omx/")) {
|
|
339
|
+
return { changed: false, gitignorePath };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const next = `${existing}${existing.endsWith("\n") || existing.length === 0 ? "" : "\n"}.omx/\n`;
|
|
343
|
+
await writeFile(gitignorePath, next);
|
|
344
|
+
return { changed: true, gitignorePath };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function buildSessionStartContext(
|
|
348
|
+
cwd: string,
|
|
349
|
+
sessionId: string,
|
|
350
|
+
): Promise<string> {
|
|
351
|
+
const sections = [
|
|
352
|
+
"OMX native SessionStart detected. Load workspace conventions from AGENTS.md, restore relevant .omx runtime/project memory context, and continue from existing mode state before making changes.",
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
const gitignoreResult = await ensureOmxGitignoreEntry(cwd);
|
|
356
|
+
if (gitignoreResult.changed) {
|
|
357
|
+
sections.push(`Added .omx/ to ${gitignoreResult.gitignorePath} to keep local OMX state out of source control.`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const modeSummaries: string[] = [];
|
|
361
|
+
for (const mode of ["ralph", "autopilot", "ultrawork", "ultraqa", "ralplan", "deep-interview", "team"] as const) {
|
|
362
|
+
const state = await readModeState(mode, cwd);
|
|
363
|
+
if (state?.active !== true || !isNonTerminalPhase(state.current_phase)) continue;
|
|
364
|
+
if (mode === "team") {
|
|
365
|
+
const teamName = safeString(state.team_name).trim();
|
|
366
|
+
if (teamName) {
|
|
367
|
+
const phase = await readTeamPhase(teamName, cwd);
|
|
368
|
+
const canonicalPhase = phase?.current_phase ?? state.current_phase;
|
|
369
|
+
if (isNonTerminalPhase(canonicalPhase)) {
|
|
370
|
+
modeSummaries.push(`- team (${teamName}) phase: ${formatPhase(canonicalPhase)}`);
|
|
371
|
+
}
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
modeSummaries.push(`- ${mode} phase: ${formatPhase(state.current_phase)}`);
|
|
376
|
+
}
|
|
377
|
+
if (modeSummaries.length > 0) {
|
|
378
|
+
sections.push(["[Active OMX modes]", ...modeSummaries].join("\n"));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const projectMemory = await readJsonIfExists(omxProjectMemoryPath(cwd));
|
|
382
|
+
if (projectMemory) {
|
|
383
|
+
const directives = Array.isArray(projectMemory.directives) ? projectMemory.directives : [];
|
|
384
|
+
const notes = Array.isArray(projectMemory.notes) ? projectMemory.notes : [];
|
|
385
|
+
const techStack = safeString(projectMemory.techStack).trim();
|
|
386
|
+
const conventions = safeString(projectMemory.conventions).trim();
|
|
387
|
+
const build = safeString(projectMemory.build).trim();
|
|
388
|
+
const summary: string[] = [];
|
|
389
|
+
if (techStack) summary.push(`- stack: ${techStack}`);
|
|
390
|
+
if (conventions) summary.push(`- conventions: ${conventions}`);
|
|
391
|
+
if (build) summary.push(`- build: ${build}`);
|
|
392
|
+
if (directives.length > 0) {
|
|
393
|
+
const firstDirective = directives[0] as Record<string, unknown>;
|
|
394
|
+
const directive = safeString(firstDirective.directive).trim();
|
|
395
|
+
if (directive) summary.push(`- directive: ${directive}`);
|
|
396
|
+
}
|
|
397
|
+
if (notes.length > 0) {
|
|
398
|
+
const firstNote = notes[0] as Record<string, unknown>;
|
|
399
|
+
const note = safeString(firstNote.content).trim();
|
|
400
|
+
if (note) summary.push(`- note: ${note}`);
|
|
401
|
+
}
|
|
402
|
+
if (summary.length > 0) {
|
|
403
|
+
sections.push(["[Project memory]", ...summary].join("\n"));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (existsSync(omxNotepadPath(cwd))) {
|
|
408
|
+
try {
|
|
409
|
+
const notepad = await readFile(omxNotepadPath(cwd), "utf-8");
|
|
410
|
+
const compact = notepad.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 3).join(" ");
|
|
411
|
+
if (compact) {
|
|
412
|
+
sections.push(`[Notepad]\n- ${compact.slice(0, 220)}`);
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
// best effort only
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const subagentSummary = await readSubagentSessionSummary(cwd, sessionId).catch(() => null);
|
|
420
|
+
if (subagentSummary && subagentSummary.activeSubagentThreadIds.length > 0) {
|
|
421
|
+
sections.push(`[Subagents]\n- active subagent threads: ${subagentSummary.activeSubagentThreadIds.length}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return sections.join("\n\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function buildAdditionalContextMessage(prompt: string, skillState?: SkillActiveState | null): string | null {
|
|
428
|
+
if (!prompt) return null;
|
|
429
|
+
const match = detectPrimaryKeyword(prompt);
|
|
430
|
+
if (!match) return null;
|
|
431
|
+
|
|
432
|
+
if (skillState?.initialized_mode && skillState.initialized_state_path) {
|
|
433
|
+
return [
|
|
434
|
+
`OMX native UserPromptSubmit detected workflow keyword "${match.keyword}" -> ${match.skill}.`,
|
|
435
|
+
`skill: ${skillState.initialized_mode} activated and initial state initialized at ${skillState.initialized_state_path}; write subsequent updates via omx_state MCP.`,
|
|
436
|
+
"Follow AGENTS.md routing and preserve ralplan/ralph execution gates.",
|
|
437
|
+
].join(" ");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return `OMX native UserPromptSubmit detected workflow keyword "${match.keyword}" -> ${match.skill}. Follow AGENTS.md routing and preserve ralplan/ralph execution gates.`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function parseTeamWorkerEnv(rawValue: string): { teamName: string; workerName: string } | null {
|
|
444
|
+
const match = /^([a-z0-9][a-z0-9-]{0,29})\/(worker-\d+)$/.exec(rawValue.trim());
|
|
445
|
+
if (!match) return null;
|
|
446
|
+
return {
|
|
447
|
+
teamName: match[1] || "",
|
|
448
|
+
workerName: match[2] || "",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function readTeamStateRootFromJson(path: string): Promise<string | null> {
|
|
453
|
+
const parsed = await readJsonIfExists(path);
|
|
454
|
+
const value = safeString(parsed?.team_state_root).trim();
|
|
455
|
+
return value || null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function resolveTeamStateDirForWorkerContext(
|
|
459
|
+
cwd: string,
|
|
460
|
+
workerContext: { teamName: string; workerName: string },
|
|
461
|
+
): Promise<string> {
|
|
462
|
+
const explicitStateRoot = safeString(process.env.OMX_TEAM_STATE_ROOT).trim();
|
|
463
|
+
if (explicitStateRoot) {
|
|
464
|
+
return resolve(cwd, explicitStateRoot);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const leaderCwd = safeString(process.env.OMX_TEAM_LEADER_CWD).trim();
|
|
468
|
+
const candidateStateDirs = [
|
|
469
|
+
...(leaderCwd ? [join(resolve(leaderCwd), ".omx", "state")] : []),
|
|
470
|
+
join(cwd, ".omx", "state"),
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
for (const candidateStateDir of candidateStateDirs) {
|
|
474
|
+
const teamRoot = join(candidateStateDir, "team", workerContext.teamName);
|
|
475
|
+
if (!existsSync(teamRoot)) continue;
|
|
476
|
+
|
|
477
|
+
const identityRoot = await readTeamStateRootFromJson(
|
|
478
|
+
join(teamRoot, "workers", workerContext.workerName, "identity.json"),
|
|
479
|
+
);
|
|
480
|
+
if (identityRoot) return resolve(cwd, identityRoot);
|
|
481
|
+
|
|
482
|
+
const manifestRoot = await readTeamStateRootFromJson(join(teamRoot, "manifest.v2.json"));
|
|
483
|
+
if (manifestRoot) return resolve(cwd, manifestRoot);
|
|
484
|
+
|
|
485
|
+
const configRoot = await readTeamStateRootFromJson(join(teamRoot, "config.json"));
|
|
486
|
+
if (configRoot) return resolve(cwd, configRoot);
|
|
487
|
+
|
|
488
|
+
return candidateStateDir;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return join(cwd, ".omx", "state");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function buildTeamWorkerStopOutput(
|
|
495
|
+
cwd: string,
|
|
496
|
+
): Promise<Record<string, unknown> | null> {
|
|
497
|
+
const workerContext = parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER));
|
|
498
|
+
if (!workerContext) return null;
|
|
499
|
+
|
|
500
|
+
const stateDir = await resolveTeamStateDirForWorkerContext(cwd, workerContext);
|
|
501
|
+
const workerRoot = join(stateDir, "team", workerContext.teamName, "workers", workerContext.workerName);
|
|
502
|
+
const [identity, status] = await Promise.all([
|
|
503
|
+
readJsonIfExists(join(workerRoot, "identity.json")),
|
|
504
|
+
readJsonIfExists(join(workerRoot, "status.json")),
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
const candidateTaskIds = new Set<string>();
|
|
508
|
+
const currentTaskId = safeString(status?.current_task_id).trim();
|
|
509
|
+
if (currentTaskId) candidateTaskIds.add(currentTaskId);
|
|
510
|
+
const assignedTasks = Array.isArray(identity?.assigned_tasks) ? identity?.assigned_tasks : [];
|
|
511
|
+
for (const taskId of assignedTasks) {
|
|
512
|
+
const normalized = safeString(taskId).trim();
|
|
513
|
+
if (normalized) candidateTaskIds.add(normalized);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (const taskId of candidateTaskIds) {
|
|
517
|
+
const task = await readJsonIfExists(
|
|
518
|
+
join(stateDir, "team", workerContext.teamName, "tasks", `task-${taskId}.json`),
|
|
519
|
+
);
|
|
520
|
+
const statusValue = safeString(task?.status).trim().toLowerCase();
|
|
521
|
+
if (!statusValue || TEAM_TERMINAL_TASK_STATUSES.has(statusValue)) continue;
|
|
522
|
+
return {
|
|
523
|
+
decision: "block",
|
|
524
|
+
reason:
|
|
525
|
+
`OMX team worker ${workerContext.workerName} is still assigned non-terminal task ${taskId} (${statusValue}); continue the current assigned task or report a concrete blocker before stopping.`,
|
|
526
|
+
stopReason: `team_worker_${workerContext.workerName}_${taskId}_${statusValue}`,
|
|
527
|
+
systemMessage:
|
|
528
|
+
`OMX team worker ${workerContext.workerName} is still assigned task ${taskId} (${statusValue}).`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function hasTeamWorkerContext(): boolean {
|
|
536
|
+
return parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER)) !== null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isStopExempt(payload: CodexHookPayload): boolean {
|
|
540
|
+
const candidates = [
|
|
541
|
+
payload.stop_reason,
|
|
542
|
+
payload.stopReason,
|
|
543
|
+
payload.reason,
|
|
544
|
+
payload.exit_reason,
|
|
545
|
+
payload.exitReason,
|
|
546
|
+
]
|
|
547
|
+
.map((value) => safeString(value).toLowerCase())
|
|
548
|
+
.filter(Boolean);
|
|
549
|
+
return candidates.some((value) =>
|
|
550
|
+
value.includes("cancel")
|
|
551
|
+
|| value.includes("abort")
|
|
552
|
+
|| value.includes("context")
|
|
553
|
+
|| value.includes("compact")
|
|
554
|
+
|| value.includes("limit"),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function buildModeBasedStopOutput(
|
|
559
|
+
mode: "autopilot" | "ultrawork" | "ultraqa",
|
|
560
|
+
cwd: string,
|
|
561
|
+
): Promise<Record<string, unknown> | null> {
|
|
562
|
+
const state = await readModeState(mode, cwd);
|
|
563
|
+
if (state?.active !== true || !isNonTerminalPhase(state.current_phase)) return null;
|
|
564
|
+
const phase = formatPhase(state.current_phase);
|
|
565
|
+
return {
|
|
566
|
+
decision: "block",
|
|
567
|
+
reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
|
|
568
|
+
stopReason: `${mode}_${phase}`,
|
|
569
|
+
systemMessage: `OMX ${mode} is still active (phase: ${phase}).`,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function buildTeamStopOutput(cwd: string): Promise<Record<string, unknown> | null> {
|
|
574
|
+
const teamState = await readModeState("team", cwd);
|
|
575
|
+
if (teamState?.active !== true) return null;
|
|
576
|
+
const teamName = safeString(teamState.team_name).trim();
|
|
577
|
+
const coarsePhase = teamState.current_phase;
|
|
578
|
+
const canonicalPhase = teamName ? (await readTeamPhase(teamName, cwd))?.current_phase ?? coarsePhase : coarsePhase;
|
|
579
|
+
if (!isNonTerminalPhase(canonicalPhase)) return null;
|
|
580
|
+
return buildTeamStopOutputForPhase(teamName, formatPhase(canonicalPhase));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildTeamStopReason(teamName: string, phase: string): string {
|
|
584
|
+
const teamContext = teamName ? ` (${teamName})` : "";
|
|
585
|
+
return `OMX team pipeline is still active${teamContext} at phase ${phase}; continue coordinating until the team reaches a terminal phase. If system-generated worker auto-checkpoint commits exist, rewrite them into Lore-format final commits before merge/finalization.`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function buildTeamStopOutputForPhase(teamName: string, phase: string): Record<string, unknown> {
|
|
589
|
+
return {
|
|
590
|
+
decision: "block",
|
|
591
|
+
reason: buildTeamStopReason(teamName, phase),
|
|
592
|
+
stopReason: `team_${phase}`,
|
|
593
|
+
systemMessage: `OMX team pipeline is still active at phase ${phase}.`,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function readPayloadSessionId(payload: CodexHookPayload): string {
|
|
598
|
+
return safeString(payload.session_id ?? payload.sessionId).trim();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function readPayloadThreadId(payload: CodexHookPayload): string {
|
|
602
|
+
return safeString(payload.thread_id ?? payload.threadId).trim();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function readPayloadTurnId(payload: CodexHookPayload): string {
|
|
606
|
+
return safeString(payload.turn_id ?? payload.turnId).trim();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function isDeepInterviewSuppressedForStop(
|
|
610
|
+
cwd: string,
|
|
611
|
+
stateDir: string,
|
|
612
|
+
sessionId: string,
|
|
613
|
+
): Promise<boolean> {
|
|
614
|
+
if (await isDeepInterviewStateActive(stateDir)) return true;
|
|
615
|
+
if (await isDeepInterviewInputLockActive(stateDir)) return true;
|
|
616
|
+
|
|
617
|
+
const scopedModeState = sessionId
|
|
618
|
+
? await readScopedJsonState("deep-interview-state.json", cwd, sessionId)
|
|
619
|
+
: null;
|
|
620
|
+
if (scopedModeState?.active === true) return true;
|
|
621
|
+
|
|
622
|
+
const scopedSkillState = sessionId
|
|
623
|
+
? await readScopedJsonState("skill-active-state.json", cwd, sessionId)
|
|
624
|
+
: null;
|
|
625
|
+
if (!scopedSkillState || scopedSkillState.active !== true) return false;
|
|
626
|
+
return safeString(scopedSkillState.skill).trim() === "deep-interview";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function buildRepeatableStopSignature(
|
|
630
|
+
payload: CodexHookPayload,
|
|
631
|
+
kind: string,
|
|
632
|
+
detail = "",
|
|
633
|
+
): string {
|
|
634
|
+
const sessionId = readPayloadSessionId(payload) || "no-session";
|
|
635
|
+
const threadId = readPayloadThreadId(payload) || "no-thread";
|
|
636
|
+
const turnId = readPayloadTurnId(payload);
|
|
637
|
+
const normalizedDetail = normalizeAutoNudgeSignatureText(detail) || safeString(detail).trim().toLowerCase();
|
|
638
|
+
const transcriptPath = safeString(payload.transcript_path ?? payload.transcriptPath).trim() || "no-transcript";
|
|
639
|
+
const lastAssistantMessage = normalizeAutoNudgeSignatureText(
|
|
640
|
+
payload.last_assistant_message ?? payload.lastAssistantMessage,
|
|
641
|
+
) || "no-message";
|
|
642
|
+
if (turnId) {
|
|
643
|
+
return [
|
|
644
|
+
kind,
|
|
645
|
+
sessionId,
|
|
646
|
+
threadId,
|
|
647
|
+
turnId,
|
|
648
|
+
transcriptPath,
|
|
649
|
+
lastAssistantMessage,
|
|
650
|
+
normalizedDetail || "no-detail",
|
|
651
|
+
].join("|");
|
|
652
|
+
}
|
|
653
|
+
return [
|
|
654
|
+
kind,
|
|
655
|
+
sessionId,
|
|
656
|
+
threadId,
|
|
657
|
+
transcriptPath,
|
|
658
|
+
lastAssistantMessage,
|
|
659
|
+
normalizedDetail || "no-detail",
|
|
660
|
+
].join("|");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function readNativeStopState(stateDir: string): Promise<Record<string, unknown>> {
|
|
664
|
+
return await readJsonIfExists(join(stateDir, NATIVE_STOP_STATE_FILE)) ?? {};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function readNativeStopSessionKey(payload: CodexHookPayload): string {
|
|
668
|
+
return readPayloadSessionId(payload) || readPayloadThreadId(payload) || "global";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function readPreviousNativeStopSignature(
|
|
672
|
+
state: Record<string, unknown>,
|
|
673
|
+
sessionKey: string,
|
|
674
|
+
): string {
|
|
675
|
+
const sessions = safeObject(state.sessions);
|
|
676
|
+
const sessionState = safeObject(sessions[sessionKey]);
|
|
677
|
+
return safeString(sessionState.last_signature).trim();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function persistNativeStopSignature(
|
|
681
|
+
stateDir: string,
|
|
682
|
+
payload: CodexHookPayload,
|
|
683
|
+
signature: string,
|
|
684
|
+
): Promise<void> {
|
|
685
|
+
if (!signature) return;
|
|
686
|
+
const state = await readNativeStopState(stateDir);
|
|
687
|
+
const sessions = safeObject(state.sessions);
|
|
688
|
+
const sessionKey = readNativeStopSessionKey(payload);
|
|
689
|
+
sessions[sessionKey] = {
|
|
690
|
+
...safeObject(sessions[sessionKey]),
|
|
691
|
+
last_signature: signature,
|
|
692
|
+
updated_at: new Date().toISOString(),
|
|
693
|
+
};
|
|
694
|
+
await writeFile(join(stateDir, NATIVE_STOP_STATE_FILE), JSON.stringify({
|
|
695
|
+
...state,
|
|
696
|
+
sessions,
|
|
697
|
+
}, null, 2));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function maybeReturnRepeatableStopOutput(
|
|
701
|
+
payload: CodexHookPayload,
|
|
702
|
+
stateDir: string,
|
|
703
|
+
signature: string,
|
|
704
|
+
output: Record<string, unknown> | null,
|
|
705
|
+
): Promise<Record<string, unknown> | null> {
|
|
706
|
+
if (!output) return null;
|
|
707
|
+
const stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
708
|
+
if (stopHookActive) {
|
|
709
|
+
const state = await readNativeStopState(stateDir);
|
|
710
|
+
const previousSignature = readPreviousNativeStopSignature(state, readNativeStopSessionKey(payload));
|
|
711
|
+
if (!signature || previousSignature === signature) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
await persistNativeStopSignature(stateDir, payload, signature);
|
|
716
|
+
return output;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function findCanonicalActiveTeamForSession(
|
|
720
|
+
cwd: string,
|
|
721
|
+
sessionId: string,
|
|
722
|
+
): Promise<{ teamName: string; phase: string } | null> {
|
|
723
|
+
if (!sessionId.trim()) return null;
|
|
724
|
+
const teamsRoot = join(resolveCanonicalTeamStateRoot(cwd), "team");
|
|
725
|
+
if (!existsSync(teamsRoot)) return null;
|
|
726
|
+
|
|
727
|
+
const entries = await readdir(teamsRoot, { withFileTypes: true }).catch(() => []);
|
|
728
|
+
for (const entry of entries) {
|
|
729
|
+
if (!entry.isDirectory()) continue;
|
|
730
|
+
const teamName = entry.name.trim();
|
|
731
|
+
if (!teamName) continue;
|
|
732
|
+
|
|
733
|
+
const [manifest, phaseState] = await Promise.all([
|
|
734
|
+
readTeamManifestV2(teamName, cwd),
|
|
735
|
+
readTeamPhase(teamName, cwd),
|
|
736
|
+
]);
|
|
737
|
+
if (!manifest || !phaseState) continue;
|
|
738
|
+
const ownerSessionId = (manifest.leader?.session_id ?? "").trim();
|
|
739
|
+
if (ownerSessionId && ownerSessionId !== sessionId.trim()) continue;
|
|
740
|
+
if (!isNonTerminalPhase(phaseState.current_phase)) continue;
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
teamName,
|
|
744
|
+
phase: formatPhase(phaseState.current_phase),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function buildSkillStopOutput(
|
|
752
|
+
cwd: string,
|
|
753
|
+
sessionId: string,
|
|
754
|
+
threadId: string,
|
|
755
|
+
): Promise<Record<string, unknown> | null> {
|
|
756
|
+
const state = await readScopedJsonState("skill-active-state.json", cwd, sessionId);
|
|
757
|
+
if (!state || state.active !== true) return null;
|
|
758
|
+
const stateSessionId = safeString(state.session_id).trim();
|
|
759
|
+
const stateThreadId = safeString(state.thread_id).trim();
|
|
760
|
+
if (sessionId && stateSessionId && stateSessionId !== sessionId) return null;
|
|
761
|
+
if (sessionId && !stateSessionId && threadId && stateThreadId && stateThreadId !== threadId) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const skill = safeString(state.skill).trim();
|
|
765
|
+
const phase = formatPhase(state.phase, "planning");
|
|
766
|
+
if (!SKILL_STOP_BLOCKERS.has(skill) || phase === "completing") return null;
|
|
767
|
+
|
|
768
|
+
const subagentSummary = await readSubagentSessionSummary(cwd, sessionId).catch(() => null);
|
|
769
|
+
if (subagentSummary && subagentSummary.activeSubagentThreadIds.length > 0) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
decision: "block",
|
|
775
|
+
reason: `OMX skill ${skill} is still active (phase: ${phase}); continue until the current ${skill} workflow reaches a terminal state.`,
|
|
776
|
+
stopReason: `skill_${skill}_${phase}`,
|
|
777
|
+
systemMessage: `OMX skill ${skill} is still active (phase: ${phase}).`,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function buildStopHookOutput(
|
|
782
|
+
payload: CodexHookPayload,
|
|
783
|
+
cwd: string,
|
|
784
|
+
stateDir: string,
|
|
785
|
+
): Promise<Record<string, unknown> | null> {
|
|
786
|
+
if (isStopExempt(payload)) {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const sessionId = readPayloadSessionId(payload);
|
|
791
|
+
const threadId = readPayloadThreadId(payload);
|
|
792
|
+
const ralphState = await readActiveRalphState(stateDir);
|
|
793
|
+
const stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
794
|
+
if (!ralphState) {
|
|
795
|
+
const teamWorkerOutput = await buildTeamWorkerStopOutput(cwd);
|
|
796
|
+
if (!stopHookActive && hasTeamWorkerContext()) return teamWorkerOutput;
|
|
797
|
+
|
|
798
|
+
const autopilotOutput = await buildModeBasedStopOutput("autopilot", cwd);
|
|
799
|
+
if (!stopHookActive && autopilotOutput) return autopilotOutput;
|
|
800
|
+
|
|
801
|
+
const ultraworkOutput = await buildModeBasedStopOutput("ultrawork", cwd);
|
|
802
|
+
if (!stopHookActive && ultraworkOutput) return ultraworkOutput;
|
|
803
|
+
|
|
804
|
+
const ultraqaOutput = await buildModeBasedStopOutput("ultraqa", cwd);
|
|
805
|
+
if (!stopHookActive && ultraqaOutput) return ultraqaOutput;
|
|
806
|
+
|
|
807
|
+
const teamOutput = await buildTeamStopOutput(cwd);
|
|
808
|
+
if (teamOutput) {
|
|
809
|
+
const teamSignature = buildRepeatableStopSignature(payload, "team-stop", safeString(teamOutput.stopReason));
|
|
810
|
+
return await maybeReturnRepeatableStopOutput(payload, stateDir, teamSignature, teamOutput);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (sessionId) {
|
|
814
|
+
const canonicalTeam = await findCanonicalActiveTeamForSession(cwd, sessionId);
|
|
815
|
+
if (canonicalTeam) {
|
|
816
|
+
const canonicalTeamOutput = buildTeamStopOutputForPhase(
|
|
817
|
+
canonicalTeam.teamName,
|
|
818
|
+
canonicalTeam.phase,
|
|
819
|
+
);
|
|
820
|
+
const canonicalTeamSignature = buildRepeatableStopSignature(payload, "team-stop", `${canonicalTeam.teamName}|${canonicalTeam.phase}`);
|
|
821
|
+
const repeatedCanonicalTeamOutput = await maybeReturnRepeatableStopOutput(
|
|
822
|
+
payload,
|
|
823
|
+
stateDir,
|
|
824
|
+
canonicalTeamSignature,
|
|
825
|
+
canonicalTeamOutput,
|
|
826
|
+
);
|
|
827
|
+
if (repeatedCanonicalTeamOutput) return repeatedCanonicalTeamOutput;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const skillOutput = await buildSkillStopOutput(cwd, sessionId, threadId);
|
|
831
|
+
if (!stopHookActive && skillOutput) return skillOutput;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const deepInterviewActive = await isDeepInterviewSuppressedForStop(cwd, stateDir, sessionId);
|
|
835
|
+
const lastAssistantMessage = safeString(
|
|
836
|
+
payload.last_assistant_message ?? payload.lastAssistantMessage,
|
|
837
|
+
);
|
|
838
|
+
const autoNudgeConfig = await loadAutoNudgeConfig();
|
|
839
|
+
|
|
840
|
+
if (
|
|
841
|
+
!deepInterviewActive
|
|
842
|
+
&& autoNudgeConfig.enabled
|
|
843
|
+
&& detectStallPattern(lastAssistantMessage, autoNudgeConfig.patterns)
|
|
844
|
+
) {
|
|
845
|
+
return await maybeReturnRepeatableStopOutput(
|
|
846
|
+
payload,
|
|
847
|
+
stateDir,
|
|
848
|
+
buildRepeatableStopSignature(payload, "auto-nudge", lastAssistantMessage),
|
|
849
|
+
{
|
|
850
|
+
decision: "block",
|
|
851
|
+
reason: autoNudgeConfig.response,
|
|
852
|
+
stopReason: "auto_nudge",
|
|
853
|
+
systemMessage:
|
|
854
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
855
|
+
},
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (stopHookActive) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const currentPhase = safeString(ralphState?.current_phase).trim() || "executing";
|
|
867
|
+
const stopReason = `ralph_${currentPhase}`;
|
|
868
|
+
const systemMessage =
|
|
869
|
+
`OMX Ralph is still active (phase: ${currentPhase}); continue the task and gather fresh verification evidence before stopping.`;
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
decision: "block",
|
|
873
|
+
reason: systemMessage,
|
|
874
|
+
stopReason,
|
|
875
|
+
systemMessage,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export async function dispatchCodexNativeHook(
|
|
880
|
+
payload: CodexHookPayload,
|
|
881
|
+
options: NativeHookDispatchOptions = {},
|
|
882
|
+
): Promise<NativeHookDispatchResult> {
|
|
883
|
+
const hookEventName = readHookEventName(payload);
|
|
884
|
+
const cwd = options.cwd ?? (safeString(payload.cwd).trim() || process.cwd());
|
|
885
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
886
|
+
await mkdir(stateDir, { recursive: true });
|
|
887
|
+
|
|
888
|
+
const omxEventName = mapCodexHookEventToOmxEvent(hookEventName);
|
|
889
|
+
let skillState: SkillActiveState | null = null;
|
|
890
|
+
|
|
891
|
+
const sessionId = safeString(payload.session_id ?? payload.sessionId).trim();
|
|
892
|
+
const threadId = safeString(payload.thread_id ?? payload.threadId).trim();
|
|
893
|
+
const turnId = safeString(payload.turn_id ?? payload.turnId).trim();
|
|
894
|
+
|
|
895
|
+
if (hookEventName === "SessionStart" && sessionId) {
|
|
896
|
+
await writeSessionStart(cwd, sessionId, {
|
|
897
|
+
pid: options.sessionOwnerPid ?? resolveSessionOwnerPid(payload),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (hookEventName === "UserPromptSubmit") {
|
|
902
|
+
const prompt = readPromptText(payload);
|
|
903
|
+
if (prompt) {
|
|
904
|
+
skillState = await recordSkillActivation({
|
|
905
|
+
stateDir,
|
|
906
|
+
text: prompt,
|
|
907
|
+
sessionId,
|
|
908
|
+
threadId,
|
|
909
|
+
turnId,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (omxEventName) {
|
|
915
|
+
const event: HookEventEnvelope = buildNativeHookEvent(
|
|
916
|
+
omxEventName,
|
|
917
|
+
buildBaseContext(cwd, payload, hookEventName!),
|
|
918
|
+
{
|
|
919
|
+
session_id: sessionId || undefined,
|
|
920
|
+
thread_id: threadId || undefined,
|
|
921
|
+
turn_id: turnId || undefined,
|
|
922
|
+
mode: safeString(payload.mode).trim() || undefined,
|
|
923
|
+
},
|
|
924
|
+
);
|
|
925
|
+
await dispatchHookEvent(event, { cwd });
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
let outputJson: Record<string, unknown> | null = null;
|
|
929
|
+
if (hookEventName === "SessionStart" || hookEventName === "UserPromptSubmit") {
|
|
930
|
+
const additionalContext = hookEventName === "SessionStart"
|
|
931
|
+
? await buildSessionStartContext(cwd, sessionId)
|
|
932
|
+
: buildAdditionalContextMessage(readPromptText(payload), skillState);
|
|
933
|
+
if (additionalContext) {
|
|
934
|
+
outputJson = {
|
|
935
|
+
hookSpecificOutput: {
|
|
936
|
+
hookEventName,
|
|
937
|
+
additionalContext,
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
} else if (hookEventName === "PreToolUse") {
|
|
942
|
+
outputJson = buildNativePreToolUseOutput(payload);
|
|
943
|
+
} else if (hookEventName === "PostToolUse") {
|
|
944
|
+
outputJson = buildNativePostToolUseOutput(payload);
|
|
945
|
+
} else if (hookEventName === "Stop") {
|
|
946
|
+
outputJson = await buildStopHookOutput(payload, cwd, stateDir);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
hookEventName,
|
|
951
|
+
omxEventName,
|
|
952
|
+
skillState,
|
|
953
|
+
outputJson,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function readStdinJson(): Promise<CodexHookPayload> {
|
|
958
|
+
const chunks: Buffer[] = [];
|
|
959
|
+
for await (const chunk of process.stdin) {
|
|
960
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
961
|
+
}
|
|
962
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
963
|
+
return raw ? safeObject(JSON.parse(raw)) : {};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export async function runCodexNativeHookCli(): Promise<void> {
|
|
967
|
+
const payload = await readStdinJson();
|
|
968
|
+
const result = await dispatchCodexNativeHook(payload);
|
|
969
|
+
if (result.outputJson) {
|
|
970
|
+
process.stdout.write(`${JSON.stringify(result.outputJson)}\n`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
975
|
+
runCodexNativeHookCli().catch((error) => {
|
|
976
|
+
process.stderr.write(
|
|
977
|
+
`[omx] codex-native-hook failed: ${
|
|
978
|
+
error instanceof Error ? error.message : String(error)
|
|
979
|
+
}\n`,
|
|
980
|
+
);
|
|
981
|
+
process.exitCode = 1;
|
|
982
|
+
});
|
|
983
|
+
}
|