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,671 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { initTeamState, readTeamConfig, saveTeamConfig, listMailboxMessages, listDispatchRequests, readDispatchRequest, createTask, updateWorkerHeartbeat, writeWorkerStatus, } from '../state.js';
|
|
9
|
+
import { executeTeamApiOperation } from '../api-interop.js';
|
|
10
|
+
import { sendWorkerMessage, broadcastWorkerMessage } from '../runtime.js';
|
|
11
|
+
import { drainPendingTeamDispatch } from '../../scripts/notify-hook/team-dispatch.js';
|
|
12
|
+
import { teamCommand } from '../../cli/team.js';
|
|
13
|
+
function buildFakeTmux(tmuxLogPath) {
|
|
14
|
+
return `#!/usr/bin/env bash
|
|
15
|
+
set -eu
|
|
16
|
+
echo "$@" >> "${tmuxLogPath}"
|
|
17
|
+
cmd="$1"
|
|
18
|
+
shift || true
|
|
19
|
+
if [[ "$cmd" == "capture-pane" ]]; then
|
|
20
|
+
printf "› ready\\n"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
if [[ "$cmd" == "display-message" ]]; then
|
|
24
|
+
target=""
|
|
25
|
+
fmt=""
|
|
26
|
+
while [[ "$#" -gt 0 ]]; do
|
|
27
|
+
case "$1" in
|
|
28
|
+
-t)
|
|
29
|
+
shift
|
|
30
|
+
target="$1"
|
|
31
|
+
;;
|
|
32
|
+
*)
|
|
33
|
+
fmt="$1"
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
shift || true
|
|
37
|
+
done
|
|
38
|
+
if [[ "$fmt" == "#{pane_in_mode}" ]]; then
|
|
39
|
+
echo "0"
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
if [[ "$fmt" == "#{pane_id}" ]]; then
|
|
43
|
+
echo "\${target:-%42}"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
if [[ "$fmt" == "#{pane_current_path}" ]]; then
|
|
47
|
+
dirname "${tmuxLogPath}"
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
if [[ "$fmt" == "#{pane_start_command}" ]]; then
|
|
51
|
+
echo "codex"
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
if [[ "$fmt" == "#{pane_current_command}" ]]; then
|
|
55
|
+
echo "codex"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
if [[ "$fmt" == "#S" ]]; then
|
|
59
|
+
echo "session-test"
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
if [[ "$cmd" == "send-keys" ]]; then
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
if [[ "$cmd" == "list-panes" ]]; then
|
|
68
|
+
printf '%%10\t111\n%%11\t112\n%%12\t113\n%%95\t195\n'
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
exit 0
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
async function writeCompatRuntimeFixture(runtimePath) {
|
|
75
|
+
await writeFile(runtimePath, `#!/usr/bin/env node
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
const path = require('path');
|
|
78
|
+
const argv = process.argv.slice(2);
|
|
79
|
+
function argValue(prefix) {
|
|
80
|
+
const entry = argv.find((value) => value.startsWith(prefix));
|
|
81
|
+
return entry ? entry.slice(prefix.length) : null;
|
|
82
|
+
}
|
|
83
|
+
function stateDir() {
|
|
84
|
+
return argValue('--state-dir=') || process.cwd();
|
|
85
|
+
}
|
|
86
|
+
function readJson(file, fallback) {
|
|
87
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return fallback; }
|
|
88
|
+
}
|
|
89
|
+
function writeJson(file, value) {
|
|
90
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
91
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\\n');
|
|
92
|
+
}
|
|
93
|
+
function nowIso() { return new Date().toISOString(); }
|
|
94
|
+
if (argv[0] === 'schema') {
|
|
95
|
+
process.stdout.write(JSON.stringify({ schema_version: 1, commands: ['acquire-authority','renew-authority','queue-dispatch','mark-notified','mark-delivered','mark-failed','request-replay','capture-snapshot'], events: [], transport: 'tmux' }) + '\\n');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
if (argv[0] !== 'exec') process.exit(1);
|
|
99
|
+
const command = JSON.parse(argv[1] || '{}');
|
|
100
|
+
const dir = stateDir();
|
|
101
|
+
const dispatchPath = path.join(dir, 'dispatch.json');
|
|
102
|
+
const mailboxPath = path.join(dir, 'mailbox.json');
|
|
103
|
+
const dispatch = readJson(dispatchPath, { records: [] });
|
|
104
|
+
const mailbox = readJson(mailboxPath, { records: [] });
|
|
105
|
+
const timestamp = nowIso();
|
|
106
|
+
switch (command.command) {
|
|
107
|
+
case 'QueueDispatch':
|
|
108
|
+
dispatch.records.push({ request_id: command.request_id, target: command.target, status: 'pending', created_at: timestamp, notified_at: null, delivered_at: null, failed_at: null, reason: null, metadata: command.metadata ?? null });
|
|
109
|
+
writeJson(dispatchPath, dispatch);
|
|
110
|
+
process.stdout.write(JSON.stringify({ event: 'DispatchQueued', request_id: command.request_id, target: command.target }) + '\\n');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
case 'MarkNotified': {
|
|
113
|
+
const record = dispatch.records.find((entry) => entry.request_id === command.request_id);
|
|
114
|
+
if (record) {
|
|
115
|
+
record.status = 'notified';
|
|
116
|
+
record.notified_at = timestamp;
|
|
117
|
+
record.reason = command.channel;
|
|
118
|
+
writeJson(dispatchPath, dispatch);
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(JSON.stringify({ event: 'DispatchNotified', request_id: command.request_id, channel: command.channel }) + '\\n');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
case 'MarkDelivered': {
|
|
124
|
+
const record = dispatch.records.find((entry) => entry.request_id === command.request_id);
|
|
125
|
+
if (record) {
|
|
126
|
+
record.status = 'delivered';
|
|
127
|
+
record.delivered_at = timestamp;
|
|
128
|
+
writeJson(dispatchPath, dispatch);
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(JSON.stringify({ event: 'DispatchDelivered', request_id: command.request_id }) + '\\n');
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
case 'CreateMailboxMessage':
|
|
134
|
+
mailbox.records.push({ message_id: command.message_id, from_worker: command.from_worker, to_worker: command.to_worker, body: command.body, created_at: timestamp, notified_at: null, delivered_at: null });
|
|
135
|
+
writeJson(mailboxPath, mailbox);
|
|
136
|
+
process.stdout.write(JSON.stringify({ event: 'MailboxMessageCreated', message_id: command.message_id, from_worker: command.from_worker, to_worker: command.to_worker }) + '\\n');
|
|
137
|
+
process.exit(0);
|
|
138
|
+
case 'MarkMailboxNotified': {
|
|
139
|
+
const record = mailbox.records.find((entry) => entry.message_id === command.message_id);
|
|
140
|
+
if (record) {
|
|
141
|
+
record.notified_at = timestamp;
|
|
142
|
+
writeJson(mailboxPath, mailbox);
|
|
143
|
+
}
|
|
144
|
+
process.stdout.write(JSON.stringify({ event: 'MailboxNotified', message_id: command.message_id }) + '\\n');
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
case 'MarkMailboxDelivered': {
|
|
148
|
+
const record = mailbox.records.find((entry) => entry.message_id === command.message_id);
|
|
149
|
+
if (record) {
|
|
150
|
+
record.delivered_at = timestamp;
|
|
151
|
+
writeJson(mailboxPath, mailbox);
|
|
152
|
+
}
|
|
153
|
+
process.stdout.write(JSON.stringify({ event: 'MailboxDelivered', message_id: command.message_id }) + '\\n');
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
process.stdout.write(JSON.stringify({ event: 'ok' }) + '\\n');
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
`);
|
|
161
|
+
await chmod(runtimePath, 0o755);
|
|
162
|
+
}
|
|
163
|
+
async function setupTeam(name, workerCount = 2) {
|
|
164
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-delivery-e2e-${name}-`));
|
|
165
|
+
await initTeamState(name, 'delivery smoke test', 'executor', workerCount, cwd);
|
|
166
|
+
return {
|
|
167
|
+
cwd,
|
|
168
|
+
cleanup: async () => await rm(cwd, { recursive: true, force: true }),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async function withFakeTmux(cwd, fn) {
|
|
172
|
+
const fakeBinDir = join(cwd, 'fake-bin');
|
|
173
|
+
const tmuxLogPath = join(cwd, 'tmux.log');
|
|
174
|
+
const previousPath = process.env.PATH;
|
|
175
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
176
|
+
await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
|
|
177
|
+
await chmod(join(fakeBinDir, 'tmux'), 0o755);
|
|
178
|
+
process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
|
|
179
|
+
try {
|
|
180
|
+
return await fn(tmuxLogPath);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
if (typeof previousPath === 'string')
|
|
184
|
+
process.env.PATH = previousPath;
|
|
185
|
+
else
|
|
186
|
+
delete process.env.PATH;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function withBridgeFixture(cwd, fn) {
|
|
190
|
+
const fakeBin = join(cwd, 'runtime-bin');
|
|
191
|
+
const runtimePath = join(fakeBin, 'omx-runtime');
|
|
192
|
+
const previousPath = process.env.PATH;
|
|
193
|
+
const previousBinary = process.env.OMX_RUNTIME_BINARY;
|
|
194
|
+
const previousBridge = process.env.OMX_RUNTIME_BRIDGE;
|
|
195
|
+
await mkdir(fakeBin, { recursive: true });
|
|
196
|
+
await writeCompatRuntimeFixture(runtimePath);
|
|
197
|
+
process.env.PATH = `${fakeBin}:${previousPath || ''}`;
|
|
198
|
+
process.env.OMX_RUNTIME_BINARY = runtimePath;
|
|
199
|
+
process.env.OMX_RUNTIME_BRIDGE = '1';
|
|
200
|
+
try {
|
|
201
|
+
return await fn(runtimePath);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
if (typeof previousPath === 'string')
|
|
205
|
+
process.env.PATH = previousPath;
|
|
206
|
+
else
|
|
207
|
+
delete process.env.PATH;
|
|
208
|
+
if (typeof previousBinary === 'string')
|
|
209
|
+
process.env.OMX_RUNTIME_BINARY = previousBinary;
|
|
210
|
+
else
|
|
211
|
+
delete process.env.OMX_RUNTIME_BINARY;
|
|
212
|
+
if (typeof previousBridge === 'string')
|
|
213
|
+
process.env.OMX_RUNTIME_BRIDGE = previousBridge;
|
|
214
|
+
else
|
|
215
|
+
delete process.env.OMX_RUNTIME_BRIDGE;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function configurePaneIds(teamName, cwd, leaderPaneId, workerPaneIds) {
|
|
219
|
+
const config = await readTeamConfig(teamName, cwd);
|
|
220
|
+
assert.ok(config, 'missing team config');
|
|
221
|
+
if (!config)
|
|
222
|
+
throw new Error('missing team config');
|
|
223
|
+
config.leader_pane_id = leaderPaneId;
|
|
224
|
+
config.workers = config.workers.map((worker) => ({
|
|
225
|
+
...worker,
|
|
226
|
+
pane_id: workerPaneIds[worker.name] ?? worker.pane_id,
|
|
227
|
+
}));
|
|
228
|
+
await saveTeamConfig(config, cwd);
|
|
229
|
+
}
|
|
230
|
+
function parseJsonLines(raw) {
|
|
231
|
+
return raw.trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
232
|
+
}
|
|
233
|
+
function buildCleanNotifyEnv(overrides = {}) {
|
|
234
|
+
return {
|
|
235
|
+
...process.env,
|
|
236
|
+
OMX_TEAM_WORKER: '',
|
|
237
|
+
OMX_TEAM_STATE_ROOT: '',
|
|
238
|
+
OMX_TEAM_LEADER_CWD: '',
|
|
239
|
+
OMX_MODEL_INSTRUCTIONS_FILE: '',
|
|
240
|
+
TMUX: '',
|
|
241
|
+
TMUX_PANE: '',
|
|
242
|
+
...overrides,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
describe('team message delivery end-to-end smoke tests', () => {
|
|
246
|
+
it('worker -> leader: send-message API creates mailbox, dispatches, and marks leader notification', async () => {
|
|
247
|
+
const { cwd, cleanup } = await setupTeam('worker-leader-api', 2);
|
|
248
|
+
try {
|
|
249
|
+
await withFakeTmux(cwd, async () => {
|
|
250
|
+
await configurePaneIds('worker-leader-api', cwd, '%95', { 'worker-1': '%10', 'worker-2': '%11' });
|
|
251
|
+
const result = await executeTeamApiOperation('send-message', {
|
|
252
|
+
team_name: 'worker-leader-api',
|
|
253
|
+
from_worker: 'worker-1',
|
|
254
|
+
to_worker: 'leader-fixed',
|
|
255
|
+
body: 'worker ack to leader',
|
|
256
|
+
}, cwd);
|
|
257
|
+
assert.equal(result.ok, true);
|
|
258
|
+
if (!result.ok)
|
|
259
|
+
throw new Error('expected successful send-message result');
|
|
260
|
+
const mailbox = await listMailboxMessages('worker-leader-api', 'leader-fixed', cwd);
|
|
261
|
+
const workerMessages = mailbox.filter((message) => message.from_worker === 'worker-1' && message.body === 'worker ack to leader');
|
|
262
|
+
assert.equal(workerMessages.length, 1);
|
|
263
|
+
assert.ok(workerMessages[0]?.notified_at, 'leader mailbox message should be notified');
|
|
264
|
+
const requests = await listDispatchRequests('worker-leader-api', cwd, { kind: 'mailbox', to_worker: 'leader-fixed' });
|
|
265
|
+
const workerRequests = requests.filter((request) => request.message_id === workerMessages[0]?.message_id);
|
|
266
|
+
assert.equal(workerRequests.length, 1);
|
|
267
|
+
assert.equal(workerRequests[0]?.status, 'notified');
|
|
268
|
+
assert.match(workerRequests[0]?.last_reason ?? '', /leader_mailbox_notified/);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
await cleanup();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
it('worker -> leader: fallback watcher nudges the leader when an undelivered worker message lingers', async () => {
|
|
276
|
+
const { cwd, cleanup } = await setupTeam('worker-leader-fallback', 1);
|
|
277
|
+
try {
|
|
278
|
+
await withFakeTmux(cwd, async (tmuxLogPath) => {
|
|
279
|
+
await configurePaneIds('worker-leader-fallback', cwd, '', { 'worker-1': '%10' });
|
|
280
|
+
await sendWorkerMessage('worker-leader-fallback', 'worker-1', 'leader-fixed', 'please read mailbox', cwd);
|
|
281
|
+
await configurePaneIds('worker-leader-fallback', cwd, '%95', { 'worker-1': '%10' });
|
|
282
|
+
await writeFile(join(cwd, '.omx', 'state', 'team-state.json'), JSON.stringify({
|
|
283
|
+
active: true,
|
|
284
|
+
team_name: 'worker-leader-fallback',
|
|
285
|
+
current_phase: 'team-exec',
|
|
286
|
+
}, null, 2));
|
|
287
|
+
await writeFile(join(cwd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
|
|
288
|
+
last_turn_at: new Date(Date.now() - 300_000).toISOString(),
|
|
289
|
+
turn_count: 9,
|
|
290
|
+
}, null, 2));
|
|
291
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
292
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
293
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', cwd, '--notify-script', notifyHook], {
|
|
294
|
+
encoding: 'utf-8',
|
|
295
|
+
env: buildCleanNotifyEnv(),
|
|
296
|
+
});
|
|
297
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
298
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
299
|
+
assert.match(tmuxLog, /send-keys -t %95 -l Team worker-leader-fallback:/);
|
|
300
|
+
assert.match(tmuxLog, /msg\(s\) pending|msg\(s\) for leader/);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
await cleanup();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
it('worker -> leader: concurrent worker sends preserve every message across mailbox and dispatch seams', async () => {
|
|
308
|
+
const { cwd, cleanup } = await setupTeam('worker-leader-concurrent', 3);
|
|
309
|
+
try {
|
|
310
|
+
await withFakeTmux(cwd, async () => {
|
|
311
|
+
await configurePaneIds('worker-leader-concurrent', cwd, '%95', {
|
|
312
|
+
'worker-1': '%10',
|
|
313
|
+
'worker-2': '%11',
|
|
314
|
+
'worker-3': '%12',
|
|
315
|
+
});
|
|
316
|
+
await Promise.all([
|
|
317
|
+
executeTeamApiOperation('send-message', { team_name: 'worker-leader-concurrent', from_worker: 'worker-1', to_worker: 'leader-fixed', body: 'msg-1' }, cwd),
|
|
318
|
+
executeTeamApiOperation('send-message', { team_name: 'worker-leader-concurrent', from_worker: 'worker-2', to_worker: 'leader-fixed', body: 'msg-2' }, cwd),
|
|
319
|
+
executeTeamApiOperation('send-message', { team_name: 'worker-leader-concurrent', from_worker: 'worker-3', to_worker: 'leader-fixed', body: 'msg-3' }, cwd),
|
|
320
|
+
]);
|
|
321
|
+
const mailbox = await listMailboxMessages('worker-leader-concurrent', 'leader-fixed', cwd);
|
|
322
|
+
const workerMessages = mailbox.filter((message) => message.from_worker.startsWith('worker-') && /^msg-[123]$/.test(message.body));
|
|
323
|
+
assert.equal(workerMessages.length, 3);
|
|
324
|
+
assert.deepEqual(new Set(workerMessages.map((message) => message.body)), new Set(['msg-1', 'msg-2', 'msg-3']));
|
|
325
|
+
assert.equal(new Set(workerMessages.map((message) => message.message_id)).size, 3);
|
|
326
|
+
assert.equal(workerMessages.filter((message) => message.notified_at).length, 3);
|
|
327
|
+
const requests = await listDispatchRequests('worker-leader-concurrent', cwd, { kind: 'mailbox', to_worker: 'leader-fixed' });
|
|
328
|
+
const workerMessageIds = new Set(workerMessages.map((message) => message.message_id));
|
|
329
|
+
const workerRequests = requests.filter((request) => request.message_id && workerMessageIds.has(request.message_id));
|
|
330
|
+
assert.equal(workerRequests.length, 3);
|
|
331
|
+
assert.equal(workerRequests.filter((request) => request.status === 'notified').length, 3);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
await cleanup();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
it('leader -> worker: leader send creates mailbox and hook notification for the target worker', async () => {
|
|
339
|
+
const { cwd, cleanup } = await setupTeam('leader-worker-send', 2);
|
|
340
|
+
try {
|
|
341
|
+
await withFakeTmux(cwd, async (tmuxLogPath) => {
|
|
342
|
+
await configurePaneIds('leader-worker-send', cwd, '%95', { 'worker-1': '%10', 'worker-2': '%11' });
|
|
343
|
+
await sendWorkerMessage('leader-worker-send', 'leader-fixed', 'worker-1', 'leader guidance', cwd);
|
|
344
|
+
const mailbox = await listMailboxMessages('leader-worker-send', 'worker-1', cwd);
|
|
345
|
+
assert.equal(mailbox.length, 1);
|
|
346
|
+
assert.equal(mailbox[0]?.body, 'leader guidance');
|
|
347
|
+
assert.ok(mailbox[0]?.notified_at);
|
|
348
|
+
const requests = await listDispatchRequests('leader-worker-send', cwd, { kind: 'mailbox', to_worker: 'worker-1' });
|
|
349
|
+
assert.equal(requests.length, 1);
|
|
350
|
+
assert.equal(requests[0]?.status, 'notified');
|
|
351
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
352
|
+
assert.match(tmuxLog, /send-keys -t %10/);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
await cleanup();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
it('leader -> worker: broadcast fans out to every worker mailbox and notification path', async () => {
|
|
360
|
+
const { cwd, cleanup } = await setupTeam('leader-broadcast', 3);
|
|
361
|
+
try {
|
|
362
|
+
await withFakeTmux(cwd, async () => {
|
|
363
|
+
await configurePaneIds('leader-broadcast', cwd, '%95', {
|
|
364
|
+
'worker-1': '%10',
|
|
365
|
+
'worker-2': '%11',
|
|
366
|
+
'worker-3': '%12',
|
|
367
|
+
});
|
|
368
|
+
await broadcastWorkerMessage('leader-broadcast', 'leader-fixed', 'broadcast hello', cwd);
|
|
369
|
+
for (const worker of ['worker-1', 'worker-2', 'worker-3']) {
|
|
370
|
+
const mailbox = await listMailboxMessages('leader-broadcast', worker, cwd);
|
|
371
|
+
assert.equal(mailbox.length, 1, `${worker} should receive one broadcast`);
|
|
372
|
+
assert.equal(mailbox[0]?.body, 'broadcast hello');
|
|
373
|
+
assert.ok(mailbox[0]?.notified_at, `${worker} should have notified_at set`);
|
|
374
|
+
}
|
|
375
|
+
const requests = await listDispatchRequests('leader-broadcast', cwd, { kind: 'mailbox' });
|
|
376
|
+
assert.equal(requests.length, 3);
|
|
377
|
+
assert.equal(requests.filter((request) => request.status === 'notified').length, 3);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
await cleanup();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
it('leader -> worker: offline worker send persists mailbox and leaves delivery pending for later pickup', async () => {
|
|
385
|
+
const { cwd, cleanup } = await setupTeam('leader-worker-offline', 1);
|
|
386
|
+
try {
|
|
387
|
+
const config = await readTeamConfig('leader-worker-offline', cwd);
|
|
388
|
+
assert.ok(config);
|
|
389
|
+
if (!config)
|
|
390
|
+
throw new Error('missing team config');
|
|
391
|
+
config.workers[0] = { ...config.workers[0], pane_id: '' };
|
|
392
|
+
await saveTeamConfig(config, cwd);
|
|
393
|
+
const result = await executeTeamApiOperation('send-message', {
|
|
394
|
+
team_name: 'leader-worker-offline',
|
|
395
|
+
from_worker: 'leader-fixed',
|
|
396
|
+
to_worker: 'worker-1',
|
|
397
|
+
body: 'pickup later',
|
|
398
|
+
}, cwd);
|
|
399
|
+
assert.equal(result.ok, true);
|
|
400
|
+
if (!result.ok)
|
|
401
|
+
throw new Error('expected successful send-message result');
|
|
402
|
+
const mailbox = await listMailboxMessages('leader-worker-offline', 'worker-1', cwd);
|
|
403
|
+
assert.equal(mailbox.length, 1);
|
|
404
|
+
assert.equal(mailbox[0]?.body, 'pickup later');
|
|
405
|
+
assert.equal(mailbox[0]?.notified_at, undefined);
|
|
406
|
+
const requests = await listDispatchRequests('leader-worker-offline', cwd, { kind: 'mailbox', to_worker: 'worker-1' });
|
|
407
|
+
assert.equal(requests.length, 1);
|
|
408
|
+
assert.equal(requests[0]?.status, 'pending');
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
await cleanup();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
it('cross-seam: TS CLI send-message flows through bridge mailbox compat and back through TS hook notification', async () => {
|
|
415
|
+
const { cwd, cleanup } = await setupTeam('cli-bridge-send', 1);
|
|
416
|
+
const previousCwd = process.cwd();
|
|
417
|
+
const logs = [];
|
|
418
|
+
const originalLog = console.log;
|
|
419
|
+
try {
|
|
420
|
+
await withBridgeFixture(cwd, async (runtimePath) => {
|
|
421
|
+
process.chdir(cwd);
|
|
422
|
+
const config = await readTeamConfig('cli-bridge-send', cwd);
|
|
423
|
+
assert.ok(config);
|
|
424
|
+
if (!config)
|
|
425
|
+
throw new Error('missing team config');
|
|
426
|
+
config.workers[0] = { ...config.workers[0], pane_id: '' };
|
|
427
|
+
await saveTeamConfig(config, cwd);
|
|
428
|
+
console.log = (...args) => logs.push(args.map(String).join(' '));
|
|
429
|
+
await teamCommand([
|
|
430
|
+
'api',
|
|
431
|
+
'send-message',
|
|
432
|
+
'--input',
|
|
433
|
+
JSON.stringify({ team_name: 'cli-bridge-send', from_worker: 'leader-fixed', to_worker: 'worker-1', body: 'cli bridge hello' }),
|
|
434
|
+
'--json',
|
|
435
|
+
]);
|
|
436
|
+
assert.equal(logs.length, 1);
|
|
437
|
+
const envelope = JSON.parse(logs[0]);
|
|
438
|
+
assert.equal(envelope.ok, true);
|
|
439
|
+
const messageId = envelope.data?.message?.message_id ?? '';
|
|
440
|
+
assert.ok(messageId);
|
|
441
|
+
await drainPendingTeamDispatch({
|
|
442
|
+
cwd,
|
|
443
|
+
maxPerTick: 5,
|
|
444
|
+
injector: async () => ({ ok: true, transport: 'hook', reason: 'injected_for_test' }),
|
|
445
|
+
});
|
|
446
|
+
const mailboxCompat = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'mailbox.json'), 'utf-8'));
|
|
447
|
+
assert.equal(mailboxCompat.records.some((record) => record.message_id === messageId), true);
|
|
448
|
+
const mailbox = await listMailboxMessages('cli-bridge-send', 'worker-1', cwd);
|
|
449
|
+
const message = mailbox.find((entry) => entry.message_id === messageId);
|
|
450
|
+
assert.ok(message, 'expected CLI-created message in canonical mailbox view');
|
|
451
|
+
assert.ok(message?.notified_at, 'expected TS hook path to persist notified_at');
|
|
452
|
+
const requests = await listDispatchRequests('cli-bridge-send', cwd, { kind: 'mailbox', to_worker: 'worker-1' });
|
|
453
|
+
assert.equal(requests[0]?.status, 'notified');
|
|
454
|
+
assert.equal(existsSync(runtimePath), true);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
console.log = originalLog;
|
|
459
|
+
process.chdir(previousCwd);
|
|
460
|
+
await cleanup();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
it('cross-seam: fallback watcher nudges for stalled workers even when the leader itself is fresh (PR #1217 guard)', async () => {
|
|
464
|
+
const { cwd, cleanup } = await setupTeam('stalled-worker-fresh-leader', 1);
|
|
465
|
+
try {
|
|
466
|
+
await withFakeTmux(cwd, async (tmuxLogPath) => {
|
|
467
|
+
await configurePaneIds('stalled-worker-fresh-leader', cwd, '%95', { 'worker-1': '%10' });
|
|
468
|
+
const task = await createTask('stalled-worker-fresh-leader', {
|
|
469
|
+
subject: 'stalled task',
|
|
470
|
+
description: 'stalled worker regression guard',
|
|
471
|
+
status: 'pending',
|
|
472
|
+
}, cwd);
|
|
473
|
+
await writeWorkerStatus('stalled-worker-fresh-leader', 'worker-1', {
|
|
474
|
+
state: 'working',
|
|
475
|
+
current_task_id: task.id,
|
|
476
|
+
updated_at: new Date().toISOString(),
|
|
477
|
+
}, cwd);
|
|
478
|
+
await updateWorkerHeartbeat('stalled-worker-fresh-leader', 'worker-1', {
|
|
479
|
+
pid: 1234,
|
|
480
|
+
turn_count: 5,
|
|
481
|
+
alive: true,
|
|
482
|
+
last_turn_at: new Date(Date.now() - 60_000).toISOString(),
|
|
483
|
+
}, cwd);
|
|
484
|
+
await writeFile(join(cwd, '.omx', 'state', 'team-state.json'), JSON.stringify({
|
|
485
|
+
active: true,
|
|
486
|
+
team_name: 'stalled-worker-fresh-leader',
|
|
487
|
+
current_phase: 'team-exec',
|
|
488
|
+
}, null, 2));
|
|
489
|
+
await writeFile(join(cwd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
|
|
490
|
+
last_turn_at: new Date().toISOString(),
|
|
491
|
+
turn_count: 2,
|
|
492
|
+
}, null, 2));
|
|
493
|
+
await writeFile(join(cwd, '.omx', 'state', 'team-leader-nudge.json'), JSON.stringify({
|
|
494
|
+
last_nudged_by_team: {},
|
|
495
|
+
last_idle_nudged_by_team: {},
|
|
496
|
+
progress_by_team: {
|
|
497
|
+
'stalled-worker-fresh-leader': {
|
|
498
|
+
signature: JSON.stringify({
|
|
499
|
+
tasks: [{ id: task.id, owner: '', status: 'pending' }],
|
|
500
|
+
workers: [{
|
|
501
|
+
worker: 'worker-1',
|
|
502
|
+
state: 'working',
|
|
503
|
+
current_task_id: task.id,
|
|
504
|
+
status_missing: false,
|
|
505
|
+
turn_count: 5,
|
|
506
|
+
heartbeat_missing: false,
|
|
507
|
+
}],
|
|
508
|
+
}),
|
|
509
|
+
last_progress_at: new Date(Date.now() - 60_000).toISOString(),
|
|
510
|
+
observed_at: new Date(Date.now() - 60_000).toISOString(),
|
|
511
|
+
missing_signal_workers: 0,
|
|
512
|
+
work_remaining: true,
|
|
513
|
+
leader_action_state: 'still_actionable',
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
}, null, 2));
|
|
517
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
518
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
519
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', cwd, '--notify-script', notifyHook], {
|
|
520
|
+
encoding: 'utf-8',
|
|
521
|
+
env: buildCleanNotifyEnv({
|
|
522
|
+
OMX_TEAM_WORKER_TURN_STALL_MS: '10000',
|
|
523
|
+
}),
|
|
524
|
+
});
|
|
525
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
526
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
527
|
+
assert.match(tmuxLog, /send-keys -t %95 -l Team stalled-worker-fresh-leader: worker panes stalled/);
|
|
528
|
+
assert.doesNotMatch(tmuxLog, /leader stale/);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
await cleanup();
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
it('cross-seam: hook-owned bridge success keeps mailbox notification visible in the canonical mailbox view', async () => {
|
|
536
|
+
const { cwd, cleanup } = await setupTeam('hook-bridge-success', 1);
|
|
537
|
+
try {
|
|
538
|
+
await withBridgeFixture(cwd, async () => {
|
|
539
|
+
const config = await readTeamConfig('hook-bridge-success', cwd);
|
|
540
|
+
assert.ok(config);
|
|
541
|
+
if (!config)
|
|
542
|
+
throw new Error('missing team config');
|
|
543
|
+
config.workers[0] = { ...config.workers[0], pane_id: '' };
|
|
544
|
+
await saveTeamConfig(config, cwd);
|
|
545
|
+
const result = await executeTeamApiOperation('send-message', {
|
|
546
|
+
team_name: 'hook-bridge-success',
|
|
547
|
+
from_worker: 'leader-fixed',
|
|
548
|
+
to_worker: 'worker-1',
|
|
549
|
+
body: 'bridge hook success',
|
|
550
|
+
}, cwd);
|
|
551
|
+
assert.equal(result.ok, true);
|
|
552
|
+
if (!result.ok)
|
|
553
|
+
throw new Error('expected successful send-message result');
|
|
554
|
+
await drainPendingTeamDispatch({
|
|
555
|
+
cwd,
|
|
556
|
+
maxPerTick: 5,
|
|
557
|
+
injector: async () => ({ ok: true, transport: 'hook', reason: 'injected_for_test' }),
|
|
558
|
+
});
|
|
559
|
+
const mailbox = await listMailboxMessages('hook-bridge-success', 'worker-1', cwd);
|
|
560
|
+
assert.equal(mailbox.length, 1);
|
|
561
|
+
assert.ok(mailbox[0]?.notified_at, 'bridge success path should preserve notified_at in canonical view');
|
|
562
|
+
const request = (await listDispatchRequests('hook-bridge-success', cwd, { kind: 'mailbox', to_worker: 'worker-1' }))[0];
|
|
563
|
+
assert.equal(request?.status, 'notified');
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
await cleanup();
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
it('edge: duplicate same from/to/body send dedupes without adding a second mailbox record', async () => {
|
|
571
|
+
const { cwd, cleanup } = await setupTeam('dedupe-smoke', 1);
|
|
572
|
+
try {
|
|
573
|
+
await withFakeTmux(cwd, async () => {
|
|
574
|
+
await configurePaneIds('dedupe-smoke', cwd, '%95', { 'worker-1': '%10' });
|
|
575
|
+
await executeTeamApiOperation('send-message', { team_name: 'dedupe-smoke', from_worker: 'worker-1', to_worker: 'leader-fixed', body: 'same-body' }, cwd);
|
|
576
|
+
await executeTeamApiOperation('send-message', { team_name: 'dedupe-smoke', from_worker: 'worker-1', to_worker: 'leader-fixed', body: 'same-body' }, cwd);
|
|
577
|
+
const mailbox = await listMailboxMessages('dedupe-smoke', 'leader-fixed', cwd);
|
|
578
|
+
const workerMessages = mailbox.filter((message) => message.from_worker === 'worker-1' && message.body === 'same-body');
|
|
579
|
+
assert.equal(workerMessages.length, 1);
|
|
580
|
+
const requests = await listDispatchRequests('dedupe-smoke', cwd, { kind: 'mailbox', to_worker: 'leader-fixed' });
|
|
581
|
+
const workerRequests = requests.filter((request) => request.message_id === workerMessages[0]?.message_id);
|
|
582
|
+
assert.equal(workerRequests.length, 1);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
await cleanup();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
it('edge: mailbox-mark-delivered updates dispatch and removes the message from active undelivered reads', async () => {
|
|
590
|
+
const { cwd, cleanup } = await setupTeam('mark-delivered-smoke', 1);
|
|
591
|
+
try {
|
|
592
|
+
await withBridgeFixture(cwd, async () => {
|
|
593
|
+
const config = await readTeamConfig('mark-delivered-smoke', cwd);
|
|
594
|
+
assert.ok(config);
|
|
595
|
+
if (!config)
|
|
596
|
+
throw new Error('missing team config');
|
|
597
|
+
config.workers[0] = { ...config.workers[0], pane_id: '' };
|
|
598
|
+
await saveTeamConfig(config, cwd);
|
|
599
|
+
const sendResult = await executeTeamApiOperation('send-message', {
|
|
600
|
+
team_name: 'mark-delivered-smoke',
|
|
601
|
+
from_worker: 'leader-fixed',
|
|
602
|
+
to_worker: 'worker-1',
|
|
603
|
+
body: 'deliver me',
|
|
604
|
+
}, cwd);
|
|
605
|
+
assert.equal(sendResult.ok, true);
|
|
606
|
+
if (!sendResult.ok)
|
|
607
|
+
throw new Error('expected send-message success');
|
|
608
|
+
const messageId = String(sendResult.data.message.message_id ?? '');
|
|
609
|
+
assert.ok(messageId);
|
|
610
|
+
await drainPendingTeamDispatch({
|
|
611
|
+
cwd,
|
|
612
|
+
maxPerTick: 5,
|
|
613
|
+
injector: async () => ({ ok: true, transport: 'hook', reason: 'injected_for_test' }),
|
|
614
|
+
});
|
|
615
|
+
const deliveredResult = await executeTeamApiOperation('mailbox-mark-delivered', {
|
|
616
|
+
team_name: 'mark-delivered-smoke',
|
|
617
|
+
worker: 'worker-1',
|
|
618
|
+
message_id: messageId,
|
|
619
|
+
}, cwd);
|
|
620
|
+
assert.equal(deliveredResult.ok, true);
|
|
621
|
+
if (!deliveredResult.ok)
|
|
622
|
+
throw new Error('expected mailbox-mark-delivered success');
|
|
623
|
+
assert.equal(deliveredResult.data.dispatch_updated, true);
|
|
624
|
+
const requestId = String(deliveredResult.data.dispatch_request_id ?? '');
|
|
625
|
+
const request = await readDispatchRequest('mark-delivered-smoke', requestId, cwd);
|
|
626
|
+
assert.equal(request?.status, 'delivered');
|
|
627
|
+
const undelivered = await executeTeamApiOperation('mailbox-list', {
|
|
628
|
+
team_name: 'mark-delivered-smoke',
|
|
629
|
+
worker: 'worker-1',
|
|
630
|
+
include_delivered: false,
|
|
631
|
+
}, cwd);
|
|
632
|
+
assert.equal(undelivered.ok, true);
|
|
633
|
+
if (!undelivered.ok)
|
|
634
|
+
throw new Error('expected mailbox-list success');
|
|
635
|
+
assert.equal(undelivered.data.count, 0);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
await cleanup();
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
it('edge: delivery still completes through the pure TS fallback path when the bridge is disabled', async () => {
|
|
643
|
+
const { cwd, cleanup } = await setupTeam('bridge-disabled-fallback', 1);
|
|
644
|
+
const previousBridge = process.env.OMX_RUNTIME_BRIDGE;
|
|
645
|
+
try {
|
|
646
|
+
process.env.OMX_RUNTIME_BRIDGE = '0';
|
|
647
|
+
await withFakeTmux(cwd, async (tmuxLogPath) => {
|
|
648
|
+
await configurePaneIds('bridge-disabled-fallback', cwd, '%95', { 'worker-1': '%10' });
|
|
649
|
+
await sendWorkerMessage('bridge-disabled-fallback', 'leader-fixed', 'worker-1', 'ts fallback only', cwd);
|
|
650
|
+
const mailbox = await listMailboxMessages('bridge-disabled-fallback', 'worker-1', cwd);
|
|
651
|
+
assert.equal(mailbox.length, 1);
|
|
652
|
+
assert.equal(mailbox[0]?.body, 'ts fallback only');
|
|
653
|
+
assert.ok(mailbox[0]?.notified_at);
|
|
654
|
+
const requests = await listDispatchRequests('bridge-disabled-fallback', cwd, { kind: 'mailbox', to_worker: 'worker-1' });
|
|
655
|
+
assert.equal(requests.length, 1);
|
|
656
|
+
assert.equal(requests[0]?.status, 'notified');
|
|
657
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
658
|
+
assert.match(tmuxLog, /send-keys -t %10/);
|
|
659
|
+
assert.equal(existsSync(join(cwd, '.omx', 'state', 'mailbox.json')), false, 'bridge compat mailbox should not be created when bridge is disabled');
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
if (typeof previousBridge === 'string')
|
|
664
|
+
process.env.OMX_RUNTIME_BRIDGE = previousBridge;
|
|
665
|
+
else
|
|
666
|
+
delete process.env.OMX_RUNTIME_BRIDGE;
|
|
667
|
+
await cleanup();
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
//# sourceMappingURL=delivery-e2e-smoke.test.js.map
|