oh-my-codex 0.20.2 → 0.20.5
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 +36 -18
- package/Cargo.toml +1 -1
- package/README.md +28 -2
- package/crates/omx-runtime/Cargo.toml +4 -0
- package/crates/omx-runtime/src/main.rs +415 -2
- package/crates/omx-runtime/tests/execution.rs +483 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +22 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +107 -22
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +63 -7
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +211 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +878 -463
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +8 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +107 -109
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +150 -8
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +95 -4
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +290 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +234 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2409 -746
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +20 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +67 -32
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +5 -7
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +83 -134
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +119 -47
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +71 -30
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +503 -30
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +27 -21
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +610 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
- 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 +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3130 -365
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +204 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1500 -223
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +124 -37
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +17 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +102 -26
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +72 -155
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +85 -101
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +82 -338
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +360 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +2 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +67 -168
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +53 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +14448 -1480
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +78 -115
- 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 +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +82 -33
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +32 -3
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +79 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +80 -4
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +3162 -26
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +61 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +462 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +8 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +73 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +58 -135
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +811 -632
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +197 -30
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3963 -428
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2405 -203
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +4099 -1039
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +4 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1527 -291
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1503 -387
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +85 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +672 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- 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 +163 -27
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1682 -376
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +114 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +25 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +67 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +16 -17
- package/skills/team/SKILL.md +13 -9
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
- package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +14238 -1429
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +91 -122
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
- package/src/scripts/notify-hook/team-worker.ts +82 -31
- package/src/scripts/notify-hook/tmux-injection.ts +33 -3
- package/src/scripts/notify-hook.ts +84 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +3608 -39
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
package/dist/team/scaling.js
CHANGED
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
* - File-based scaling lock prevents concurrent scale operations
|
|
10
10
|
* - 'draining' worker status for graceful transitions during scale_down
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
14
|
+
import { mkdir, readFile, realpath, rm, writeFile } from 'fs/promises';
|
|
15
|
+
import { existsSync } from 'fs';
|
|
16
|
+
import { sanitizeTeamName, isTmuxAvailable, waitForWorkerReady, dismissTrustPromptIfPresent, isWorkerAlive, buildWorkerStartupCommand, trustWorkerMiseConfigIfAvailable, writeWorkerStartupScriptCommand, resolveTeamWorkerCliForResolvedLaunchArgs, assertTeamWorkerCliPolicyCompatibility, readPaneTeamOwnerTagResult, teardownWorkerPanes, } from './tmux-session.js';
|
|
17
|
+
import { readExactPaneProofSync } from './exact-pane.js';
|
|
18
|
+
import { spawnSync } from 'child_process';
|
|
19
|
+
import { teamReadConfig as readTeamConfig, teamSaveConfig as saveTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadManifest as readTeamManifestV2, teamNormalizePolicy as normalizeTeamPolicy, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerStatus as writeWorkerStatus, teamWithScalingLock as withScalingLock, teamWithTaskMembershipBarrier as withTaskMembershipBarrier, recoverTeamMembershipTaskTransaction, commitTeamMembershipTaskTransaction, finalizeTeamMembershipTaskTransaction, writeAtomic, teamRemoveDurableFile as removeDurableFile, teamAppendEvent as appendTeamEvent, teamCreateTask as createStateTask, teamListTasks as listTasks, teamReadTask as readTask, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamReadDispatchRequest as readDispatchRequest, teamTransitionDispatchRequest as transitionDispatchRequest, teamRemoveDispatchRequestsForWorkers as removeDispatchRequestsForWorkers, } from './team-ops.js';
|
|
17
20
|
import { queueInboxInstruction, waitForDispatchReceipt, } from './mcp-comm.js';
|
|
18
21
|
import { generateInitialInbox, buildTriggerDirective, writeWorkerRoleInstructionsFile, writeWorkerWorktreeRootAgentsFile, removeWorkerWorktreeRootAgentsFile, } from './worker-bootstrap.js';
|
|
19
22
|
import { buildTeamWorkerGoalInstruction } from './goal-workflow.js';
|
|
@@ -24,8 +27,29 @@ import { resolveCodexHomeForLaunch } from '../cli/codex-home.js';
|
|
|
24
27
|
import { parseTeamWorkerLaunchArgs, resolveTeamWorkerLaunchArgs, resolveAgentDefaultModel, resolveAgentReasoningEffort, shouldHonorAgentExactModel, TEAM_WORKER_INHERITED_MODEL_ENV, } from './model-contract.js';
|
|
25
28
|
import { resolveCanonicalTeamStateRoot } from './state-root.js';
|
|
26
29
|
import { ensureWorktree, planWorktreeTarget, rollbackProvisionedWorktrees, } from './worktree.js';
|
|
30
|
+
import { withTaskClaimLock } from './state/locks.js';
|
|
27
31
|
import { buildApprovedTeamHandoffSection, resolvePersistedApprovedTeamExecutionContinuityState, } from './approved-execution.js';
|
|
28
32
|
import { readPersistedTeamUltragoalContext, renderLeaderOwnedUltragoalContextSection, } from './ultragoal-context.js';
|
|
33
|
+
const TASK_CLAIM_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
34
|
+
async function withTaskClaimLocks(teamName, taskIds, cwd, fn) {
|
|
35
|
+
const uniqueTaskIds = [...new Set(taskIds)].sort((left, right) => Number(left) - Number(right));
|
|
36
|
+
const teamDir = (name, lockCwd) => join(resolveCanonicalTeamStateRoot(lockCwd), 'team', name);
|
|
37
|
+
const locks = {
|
|
38
|
+
teamDir,
|
|
39
|
+
taskClaimLockDir: (name, taskId, lockCwd) => (join(teamDir(name, lockCwd), 'claims', `task-${taskId}.lock`)),
|
|
40
|
+
mailboxLockDir: (name, workerName, lockCwd) => (join(teamDir(name, lockCwd), 'mailbox', `.lock-${workerName}`)),
|
|
41
|
+
};
|
|
42
|
+
const acquire = async (index) => {
|
|
43
|
+
if (index >= uniqueTaskIds.length)
|
|
44
|
+
return await fn();
|
|
45
|
+
const taskId = uniqueTaskIds[index];
|
|
46
|
+
const locked = await withTaskClaimLock(teamName, taskId, cwd, TASK_CLAIM_LOCK_STALE_MS, locks, async () => await acquire(index + 1));
|
|
47
|
+
if (!locked.ok)
|
|
48
|
+
throw new Error(`Timed out acquiring task claim lock for ${teamName}/${taskId}`);
|
|
49
|
+
return locked.value;
|
|
50
|
+
};
|
|
51
|
+
return await acquire(0);
|
|
52
|
+
}
|
|
29
53
|
// ── Environment gate ──────────────────────────────────────────────────────────
|
|
30
54
|
const OMX_TEAM_SCALING_ENABLED_ENV = 'OMX_TEAM_SCALING_ENABLED';
|
|
31
55
|
const WORKTREE_TRIGGER_STATE_ROOT = '$OMX_TEAM_STATE_ROOT';
|
|
@@ -36,6 +60,94 @@ export function isScalingEnabled(env = process.env) {
|
|
|
36
60
|
const normalized = raw.trim().toLowerCase();
|
|
37
61
|
return ['1', 'true', 'yes', 'on', 'enabled'].includes(normalized);
|
|
38
62
|
}
|
|
63
|
+
const SCALE_PANE_INSTANCE_FORMAT = '#{pane_id}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{window_id}\t#{@omx_team_pane_owner_id}';
|
|
64
|
+
const SCALE_SPLIT_RECEIPT_FORMAT = '#{pane_id}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{window_id}';
|
|
65
|
+
function scaleTransactionReceipt() {
|
|
66
|
+
return `omx-scale-${randomUUID()}`;
|
|
67
|
+
}
|
|
68
|
+
function hasExactScaleReceipt(stdout, receipt) {
|
|
69
|
+
return stdout === `${receipt}\n` || stdout === `${receipt}\r\n`;
|
|
70
|
+
}
|
|
71
|
+
function runScalePaneTransaction(pane, effect, receipt = scaleTransactionReceipt()) {
|
|
72
|
+
const result = spawnSync('tmux', [
|
|
73
|
+
'if-shell', '-F', '-t', pane.paneId, buildScalePaneAuthorityCondition(pane),
|
|
74
|
+
`${effect} \\; display-message -p ${quoteTmuxCommand(receipt)}`,
|
|
75
|
+
"display-message -p ''",
|
|
76
|
+
], { encoding: 'utf-8' });
|
|
77
|
+
return result.status === 0 && hasExactScaleReceipt(result.stdout || '', receipt);
|
|
78
|
+
}
|
|
79
|
+
function tagScalePaneWithAuthority(pane, teamOwnerId) {
|
|
80
|
+
const comparisons = [
|
|
81
|
+
['pane_id', pane.paneId],
|
|
82
|
+
['pane_pid', String(pane.pid)],
|
|
83
|
+
['session_name', pane.sessionName],
|
|
84
|
+
['session_id', pane.sessionId],
|
|
85
|
+
['window_id', pane.windowId],
|
|
86
|
+
].map(([field, expected]) => `#{==:#{${field}},${expected}}`);
|
|
87
|
+
const condition = comparisons.reduce((combined, comparison) => `#{&&:${combined},${comparison}}`);
|
|
88
|
+
const receipt = scaleTransactionReceipt();
|
|
89
|
+
const result = spawnSync('tmux', [
|
|
90
|
+
'if-shell', '-F', '-t', pane.paneId, condition,
|
|
91
|
+
`set-option -p -t ${pane.paneId} @omx_team_pane_owner_id ${quoteTmuxCommand(teamOwnerId)} \\; display-message -p ${quoteTmuxCommand(receipt)}`,
|
|
92
|
+
"display-message -p ''",
|
|
93
|
+
], { encoding: 'utf-8' });
|
|
94
|
+
if (result.status !== 0 || !hasExactScaleReceipt(result.stdout || '', receipt))
|
|
95
|
+
return null;
|
|
96
|
+
return { ...pane, teamOwnerId };
|
|
97
|
+
}
|
|
98
|
+
function killScalePaneWithAuthority(pane) {
|
|
99
|
+
return runScalePaneTransaction(pane, `kill-pane -t ${pane.paneId}`);
|
|
100
|
+
}
|
|
101
|
+
function parseScalePaneInstance(value) {
|
|
102
|
+
const fields = value.trim().split('\t');
|
|
103
|
+
if (fields.length !== 6)
|
|
104
|
+
return null;
|
|
105
|
+
const [paneId, pidText, sessionName, sessionId, windowId, teamOwnerId] = fields;
|
|
106
|
+
const pid = Number(pidText);
|
|
107
|
+
if (!/^%[0-9]+$/.test(paneId) || !Number.isSafeInteger(pid) || pid <= 0
|
|
108
|
+
|| !sessionName || !/^\$[0-9]+$/.test(sessionId) || !/^@[0-9]+$/.test(windowId) || !teamOwnerId)
|
|
109
|
+
return null;
|
|
110
|
+
return { paneId, pid, sessionName, sessionId, windowId, teamOwnerId };
|
|
111
|
+
}
|
|
112
|
+
function readScalePaneInstance(paneId) {
|
|
113
|
+
const result = spawnSync('tmux', ['display-message', '-p', '-t', paneId, SCALE_PANE_INSTANCE_FORMAT], { encoding: 'utf-8' });
|
|
114
|
+
return result.status === 0 ? parseScalePaneInstance(result.stdout || '') : null;
|
|
115
|
+
}
|
|
116
|
+
function quoteTmuxCommand(value) {
|
|
117
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
118
|
+
}
|
|
119
|
+
function buildScalePaneAuthorityCondition(instance) {
|
|
120
|
+
const comparisons = [
|
|
121
|
+
['pane_id', instance.paneId],
|
|
122
|
+
['pane_pid', String(instance.pid)],
|
|
123
|
+
['session_name', instance.sessionName],
|
|
124
|
+
['session_id', instance.sessionId],
|
|
125
|
+
['window_id', instance.windowId],
|
|
126
|
+
['@omx_team_pane_owner_id', instance.teamOwnerId],
|
|
127
|
+
].map(([field, expected]) => `#{==:#{${field}},${expected}}`);
|
|
128
|
+
return comparisons.reduce((condition, comparison) => `#{&&:${condition},${comparison}}`);
|
|
129
|
+
}
|
|
130
|
+
function splitWorkerPaneWithScaleAuthority(source, splitDirection, workerCwd, command) {
|
|
131
|
+
const receipt = scaleTransactionReceipt();
|
|
132
|
+
const splitCommand = [
|
|
133
|
+
'split-window', splitDirection, '-t', source.paneId, '-d', '-P', '-F', `${SCALE_SPLIT_RECEIPT_FORMAT}\t${receipt}`,
|
|
134
|
+
'-c', workerCwd, command,
|
|
135
|
+
].map(quoteTmuxCommand).join(' ');
|
|
136
|
+
const result = spawnSync('tmux', [
|
|
137
|
+
'if-shell', '-F', '-t', source.paneId, buildScalePaneAuthorityCondition(source), splitCommand, "display-message -p ''",
|
|
138
|
+
], { encoding: 'utf-8' });
|
|
139
|
+
if (result.status !== 0)
|
|
140
|
+
return null;
|
|
141
|
+
const fields = (result.stdout || '').replace(/\r?\n$/, '').split('\t');
|
|
142
|
+
if (fields.length !== 6 || fields[5] !== receipt)
|
|
143
|
+
return null;
|
|
144
|
+
const [paneId, pidText, sessionName, sessionId, windowId] = fields;
|
|
145
|
+
const pid = Number(pidText);
|
|
146
|
+
if (!/^%[0-9]+$/.test(paneId) || !Number.isSafeInteger(pid) || pid <= 0
|
|
147
|
+
|| !sessionName || !sessionId || !/^\$[0-9]+$/.test(sessionId) || !/^@[0-9]+$/.test(windowId))
|
|
148
|
+
return null;
|
|
149
|
+
return { paneId, pid, sessionName, sessionId, windowId };
|
|
150
|
+
}
|
|
39
151
|
function assertScalingEnabled(env = process.env) {
|
|
40
152
|
if (!isScalingEnabled(env)) {
|
|
41
153
|
throw new Error(`Dynamic scaling is disabled. Set ${OMX_TEAM_SCALING_ENABLED_ENV}=1 to enable.`);
|
|
@@ -80,6 +192,67 @@ function buildScaleUpWorkerLaunchPlans(params) {
|
|
|
80
192
|
function resolveInstructionStateRoot(worktreePath) {
|
|
81
193
|
return worktreePath ? WORKTREE_TRIGGER_STATE_ROOT : undefined;
|
|
82
194
|
}
|
|
195
|
+
function hasScaleUpFailureInjection(env, phase) {
|
|
196
|
+
return env.OMX_TEAM_SCALE_UP_INJECT_FAILURE?.split(',').map((value) => value.trim()).includes(phase) ?? false;
|
|
197
|
+
}
|
|
198
|
+
function throwIfScaleUpFailureInjected(env, phase) {
|
|
199
|
+
if (hasScaleUpFailureInjection(env, phase)) {
|
|
200
|
+
throw new Error(`injected_scale_up_failure:${phase}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function recoverCreatedWorktreeAfterEnsureFailure(plan, worktreePathExistedBeforeEnsure, branchExistedBeforeEnsure) {
|
|
204
|
+
if (worktreePathExistedBeforeEnsure || !existsSync(plan.worktreePath))
|
|
205
|
+
return null;
|
|
206
|
+
const commonDir = spawnSync('git', ['rev-parse', '--git-common-dir'], {
|
|
207
|
+
cwd: plan.worktreePath,
|
|
208
|
+
encoding: 'utf-8',
|
|
209
|
+
windowsHide: true,
|
|
210
|
+
});
|
|
211
|
+
const repoCommonDir = spawnSync('git', ['rev-parse', '--git-common-dir'], {
|
|
212
|
+
cwd: plan.repoRoot,
|
|
213
|
+
encoding: 'utf-8',
|
|
214
|
+
windowsHide: true,
|
|
215
|
+
});
|
|
216
|
+
if (commonDir.status !== 0
|
|
217
|
+
|| repoCommonDir.status !== 0
|
|
218
|
+
|| resolve(plan.worktreePath, (commonDir.stdout || '').trim())
|
|
219
|
+
!== resolve(plan.repoRoot, (repoCommonDir.stdout || '').trim())) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (plan.branchName) {
|
|
223
|
+
const branch = spawnSync('git', ['symbolic-ref', '-q', 'HEAD'], {
|
|
224
|
+
cwd: plan.worktreePath,
|
|
225
|
+
encoding: 'utf-8',
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
});
|
|
228
|
+
if (branch.status !== 0 || (branch.stdout || '').trim() !== `refs/heads/${plan.branchName}`)
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const head = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
233
|
+
cwd: plan.worktreePath,
|
|
234
|
+
encoding: 'utf-8',
|
|
235
|
+
windowsHide: true,
|
|
236
|
+
});
|
|
237
|
+
const branch = spawnSync('git', ['symbolic-ref', '-q', 'HEAD'], {
|
|
238
|
+
cwd: plan.worktreePath,
|
|
239
|
+
encoding: 'utf-8',
|
|
240
|
+
windowsHide: true,
|
|
241
|
+
});
|
|
242
|
+
if (head.status !== 0 || branch.status === 0 || (head.stdout || '').trim() !== plan.baseRef)
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
enabled: true,
|
|
247
|
+
repoRoot: plan.repoRoot,
|
|
248
|
+
worktreePath: plan.worktreePath,
|
|
249
|
+
detached: plan.detached,
|
|
250
|
+
branchName: plan.branchName,
|
|
251
|
+
created: true,
|
|
252
|
+
reused: false,
|
|
253
|
+
createdBranch: Boolean(plan.branchName && !branchExistedBeforeEnsure),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
83
256
|
function assertUnreachableApprovedExecutionState(state) {
|
|
84
257
|
throw new Error(`unreachable_scale_up_approved_execution_state:${JSON.stringify(state)}`);
|
|
85
258
|
}
|
|
@@ -146,9 +319,22 @@ function resolveScaleUpWorktreeMode(config) {
|
|
|
146
319
|
throw error;
|
|
147
320
|
}
|
|
148
321
|
}
|
|
149
|
-
async function notifyWorkerPaneOutcome(sessionName,
|
|
322
|
+
async function notifyWorkerPaneOutcome(sessionName, message, paneId, expectedPanePid, expectedTeamOwnerId, expectedSessionId, expectedWindowId) {
|
|
150
323
|
try {
|
|
151
|
-
|
|
324
|
+
if (!paneId || !expectedPanePid || !expectedTeamOwnerId || !expectedSessionId || !expectedWindowId) {
|
|
325
|
+
throw new Error(`scale_up_final_send_authority_missing:${paneId ?? 'unknown'}`);
|
|
326
|
+
}
|
|
327
|
+
const pane = {
|
|
328
|
+
paneId,
|
|
329
|
+
pid: expectedPanePid,
|
|
330
|
+
sessionName,
|
|
331
|
+
sessionId: expectedSessionId,
|
|
332
|
+
windowId: expectedWindowId,
|
|
333
|
+
teamOwnerId: expectedTeamOwnerId,
|
|
334
|
+
};
|
|
335
|
+
if (!runScalePaneTransaction(pane, `send-keys -t ${paneId} -l ${quoteTmuxCommand(message)} \\; send-keys -t ${paneId} C-m`)) {
|
|
336
|
+
throw new Error(`scale_up_final_send_authority_lost:${paneId}`);
|
|
337
|
+
}
|
|
152
338
|
return { ok: true, transport: 'tmux_send_keys', reason: 'tmux_send_keys_sent' };
|
|
153
339
|
}
|
|
154
340
|
catch (error) {
|
|
@@ -177,356 +363,1082 @@ export async function scaleUp(teamName, count, agentType, tasks, cwd, env = proc
|
|
|
177
363
|
const sanitized = sanitizeTeamName(teamName);
|
|
178
364
|
const leaderCwd = resolve(cwd);
|
|
179
365
|
return await withScalingLock(sanitized, leaderCwd, async () => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
existingTasks,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
366
|
+
return await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
|
|
367
|
+
const config = await readTeamConfig(sanitized, leaderCwd);
|
|
368
|
+
if (!config) {
|
|
369
|
+
return { ok: false, error: `Team ${sanitized} not found` };
|
|
370
|
+
}
|
|
371
|
+
const originalConfig = JSON.parse(JSON.stringify(config));
|
|
372
|
+
const configPath = join(resolveCanonicalTeamStateRoot(leaderCwd), 'team', sanitized, 'config.json');
|
|
373
|
+
const manifestPath = join(resolveCanonicalTeamStateRoot(leaderCwd), 'team', sanitized, 'manifest.v2.json');
|
|
374
|
+
const originalManifestBytes = existsSync(manifestPath) ? await readFile(manifestPath, 'utf8') : null;
|
|
375
|
+
const maxWorkers = config.max_workers;
|
|
376
|
+
const currentCount = config.workers.length;
|
|
377
|
+
if (currentCount + count > maxWorkers) {
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
error: `Cannot add ${count} workers: would exceed max_workers (${currentCount} + ${count} > ${maxWorkers})`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const teamStateRoot = config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
|
|
384
|
+
const codexHomeOverride = resolveCodexHomeForLaunch(leaderCwd, env);
|
|
385
|
+
const launchEnv = codexHomeOverride
|
|
386
|
+
? { ...env, CODEX_HOME: codexHomeOverride }
|
|
387
|
+
: env;
|
|
388
|
+
// Build and validate every launch plan before any task, directory, worktree,
|
|
389
|
+
// pane, process, or config mutation. The plan is the sole source of launch
|
|
390
|
+
// policy; later task materialization must not alter it.
|
|
391
|
+
const initialNextIndex = config.next_worker_index ?? (currentCount + 1);
|
|
392
|
+
let workerLaunchPlans;
|
|
393
|
+
try {
|
|
394
|
+
const existingTasks = await listTasks(sanitized, leaderCwd);
|
|
395
|
+
workerLaunchPlans = buildScaleUpWorkerLaunchPlans({
|
|
396
|
+
count,
|
|
397
|
+
nextWorkerIndex: initialNextIndex,
|
|
398
|
+
agentType,
|
|
399
|
+
existingTasks,
|
|
400
|
+
incomingTasks: tasks,
|
|
401
|
+
launchEnv,
|
|
402
|
+
codexHomeOverride,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
error: error instanceof Error ? error.message : String(error),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
let nextIndex = initialNextIndex;
|
|
412
|
+
const sessionName = config.tmux_session;
|
|
413
|
+
const manifest = await readTeamManifestV2(sanitized, leaderCwd);
|
|
414
|
+
const dispatchPolicy = normalizeTeamPolicy(manifest?.policy, {
|
|
415
|
+
display_mode: manifest?.policy?.display_mode === 'split_pane' ? 'split_pane' : 'auto',
|
|
416
|
+
worker_launch_mode: config.worker_launch_mode,
|
|
212
417
|
});
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
418
|
+
const approvedExecutionState = await resolvePersistedApprovedTeamExecutionContinuityState(sanitized, config.leader_cwd ?? leaderCwd, config.team_state_root ?? teamStateRoot);
|
|
419
|
+
const approvedExecutionGate = resolveScaleUpApprovedExecutionGate(sanitized, approvedExecutionState);
|
|
420
|
+
if (!approvedExecutionGate.ok) {
|
|
421
|
+
return approvedExecutionGate;
|
|
422
|
+
}
|
|
423
|
+
const persistedUltragoalContext = await readPersistedTeamUltragoalContext(sanitized, config.leader_cwd ?? leaderCwd, config.team_state_root ?? teamStateRoot);
|
|
424
|
+
const approvedContextSection = joinContextSections(approvedExecutionGate.approvedContextSection, renderLeaderOwnedUltragoalContextSection(persistedUltragoalContext));
|
|
425
|
+
const initialSplitTargetCandidate = config.workers[config.workers.length - 1];
|
|
426
|
+
const initialSplitTargetWorker = initialSplitTargetCandidate?.pane_id?.trim()
|
|
427
|
+
? initialSplitTargetCandidate
|
|
428
|
+
: undefined;
|
|
429
|
+
const initialSplitTarget = initialSplitTargetWorker?.pane_id ?? config.leader_pane_id ?? '';
|
|
430
|
+
const initialSplitTargetOwnerId = typeof config.tmux_pane_owner_id === 'string'
|
|
431
|
+
? config.tmux_pane_owner_id.trim()
|
|
432
|
+
: '';
|
|
433
|
+
if (!initialSplitTargetOwnerId) {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
error: `scale_up_split_target_owner_unavailable:${initialSplitTarget}`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
let initialExpectedSplitTargetPid = initialSplitTargetWorker
|
|
440
|
+
? initialSplitTargetWorker.pid
|
|
441
|
+
: config.leader_pane_pid;
|
|
442
|
+
if (typeof initialExpectedSplitTargetPid !== 'number'
|
|
443
|
+
|| !Number.isSafeInteger(initialExpectedSplitTargetPid)
|
|
444
|
+
|| initialExpectedSplitTargetPid <= 0) {
|
|
445
|
+
// Legacy configs may lack a persisted PID, but the persisted pane ID and
|
|
446
|
+
// canonical owner remain mandatory. Pin only the exact live pane proven
|
|
447
|
+
// from one global snapshot, then re-prove it after reading the owner tag
|
|
448
|
+
// so a recycled pane ID cannot be backfilled.
|
|
449
|
+
const migrationProof = readExactPaneProofSync(initialSplitTarget);
|
|
450
|
+
if (migrationProof.status !== 'live') {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
error: `scale_up_split_target_proof_unavailable:${initialSplitTarget}:${migrationProof.reason}`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const migrationOwner = readPaneTeamOwnerTagResult(migrationProof.paneId);
|
|
457
|
+
if (migrationOwner.status !== 'value' || migrationOwner.value !== initialSplitTargetOwnerId) {
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
error: `scale_up_split_target_owner_changed:${migrationProof.paneId}`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const finalMigrationProof = readExactPaneProofSync(migrationProof.paneId);
|
|
464
|
+
if (finalMigrationProof.status !== 'live') {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
error: `scale_up_split_target_proof_unavailable:${migrationProof.paneId}:${finalMigrationProof.reason}`,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (finalMigrationProof.pid !== migrationProof.pid) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
error: `scale_up_split_target_pid_changed:${migrationProof.paneId}:${migrationProof.pid}:${finalMigrationProof.pid}`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (initialSplitTargetWorker) {
|
|
477
|
+
initialSplitTargetWorker.pid = finalMigrationProof.pid;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
config.leader_pane_pid = finalMigrationProof.pid;
|
|
246
481
|
}
|
|
247
482
|
try {
|
|
248
|
-
|
|
249
|
-
execFileSync('tmux', ['kill-pane', '-t', w.pane_id], { stdio: 'pipe',
|
|
250
|
-
windowsHide: true,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
483
|
+
await saveTeamConfig(config, leaderCwd);
|
|
253
484
|
}
|
|
254
|
-
catch {
|
|
255
|
-
|
|
256
|
-
|
|
485
|
+
catch (error) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
error: `scale_up_split_target_pid_persistence_failed:${initialSplitTarget}:${error instanceof Error ? error.message : String(error)}`,
|
|
489
|
+
};
|
|
257
490
|
}
|
|
491
|
+
Object.assign(originalConfig, JSON.parse(JSON.stringify(config)));
|
|
492
|
+
initialExpectedSplitTargetPid = finalMigrationProof.pid;
|
|
493
|
+
}
|
|
494
|
+
const initialSplitTargetProof = readExactPaneProofSync(initialSplitTarget);
|
|
495
|
+
if (initialSplitTargetProof.status !== 'live') {
|
|
496
|
+
return {
|
|
497
|
+
ok: false,
|
|
498
|
+
error: `scale_up_split_target_proof_unavailable:${initialSplitTarget}:${initialSplitTargetProof.reason}`,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (initialSplitTargetProof.pid !== initialExpectedSplitTargetPid) {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
error: `scale_up_split_target_pid_changed:${initialSplitTarget}:${initialExpectedSplitTargetPid}:${initialSplitTargetProof.pid}`,
|
|
505
|
+
};
|
|
258
506
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
await removeWorkerWorktreeRootAgentsFile(sanitized, context.workerName, teamStateRoot, context.worktreePath).catch(() => { });
|
|
507
|
+
const effectiveWorktreeMode = config.worktree_mode ?? resolveScaleUpWorktreeMode(config);
|
|
508
|
+
if (!config.worktree_mode && effectiveWorktreeMode.enabled) {
|
|
509
|
+
config.worktree_mode = effectiveWorktreeMode;
|
|
263
510
|
}
|
|
264
|
-
|
|
511
|
+
const addedWorkers = [];
|
|
512
|
+
// A scale-up pane becomes killable during rollback only after its exact
|
|
513
|
+
// process identity was pinned and, when Team ownership is configured, its
|
|
514
|
+
// owner tag command completed. A returned pane ID alone is cleanup debt,
|
|
515
|
+
// never authority to affect a potentially recycled pane.
|
|
516
|
+
const rollbackTaggedPaneOwnerIds = new Map();
|
|
517
|
+
const createdTaskIds = [];
|
|
518
|
+
const provisionedWorktrees = [];
|
|
519
|
+
const preparedWorkerDirectoryOwner = new Map();
|
|
520
|
+
const preparedStartupScriptOwner = new Map();
|
|
521
|
+
const runtimeDirectoryPath = join(teamStateRoot, 'team', sanitized, 'runtime');
|
|
522
|
+
const runtimeDirectoryExisted = existsSync(runtimeDirectoryPath);
|
|
523
|
+
const rollbackScaleUp = async (error, context = {}) => {
|
|
524
|
+
const rollbackWorkers = [...new Map([...addedWorkers, context.worker]
|
|
525
|
+
.filter((worker) => Boolean(worker))
|
|
526
|
+
.map((worker) => [worker.name, worker])).values()];
|
|
527
|
+
const rollbackWorkerNames = new Set(rollbackWorkers.map((worker) => worker.name));
|
|
528
|
+
// Persist every concrete rollback worker before pane teardown. If the
|
|
529
|
+
// process exits after this point, canonical config/manifest state still
|
|
530
|
+
// identifies each live pane and its retryable resources.
|
|
265
531
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
532
|
+
await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
|
|
533
|
+
await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
|
|
534
|
+
const currentConfigBytes = await readFile(configPath, 'utf8');
|
|
535
|
+
const currentManifestBytes = existsSync(manifestPath) ? await readFile(manifestPath, 'utf8') : null;
|
|
536
|
+
const currentConfig = JSON.parse(currentConfigBytes);
|
|
537
|
+
const trackedByName = new Map(currentConfig.workers.map((worker) => [worker.name, worker]));
|
|
538
|
+
for (const worker of rollbackWorkers)
|
|
539
|
+
trackedByName.set(worker.name, worker);
|
|
540
|
+
const trackedConfig = {
|
|
541
|
+
...currentConfig,
|
|
542
|
+
workers: [...trackedByName.values()],
|
|
543
|
+
worker_count: trackedByName.size,
|
|
544
|
+
next_worker_index: Math.max(currentConfig.next_worker_index ?? (currentConfig.workers.length + 1), ...rollbackWorkers.map((worker) => worker.index + 1)),
|
|
545
|
+
};
|
|
546
|
+
const trackedManifestBytes = currentManifestBytes === null
|
|
547
|
+
? null
|
|
548
|
+
: JSON.stringify({
|
|
549
|
+
...JSON.parse(currentManifestBytes),
|
|
550
|
+
workers: trackedConfig.workers,
|
|
551
|
+
worker_count: trackedConfig.worker_count,
|
|
552
|
+
next_worker_index: trackedConfig.next_worker_index,
|
|
553
|
+
}, null, 2);
|
|
554
|
+
await commitTeamMembershipTaskTransaction(sanitized, leaderCwd, {
|
|
555
|
+
baseGeneration: currentConfig.config_generation ?? 0,
|
|
556
|
+
tasks: [],
|
|
557
|
+
config: { oldBytes: currentConfigBytes, newBytes: JSON.stringify(trackedConfig, null, 2) },
|
|
558
|
+
manifest: { oldBytes: currentManifestBytes, newBytes: trackedManifestBytes },
|
|
559
|
+
recoverToNewOnFailure: true,
|
|
560
|
+
});
|
|
561
|
+
const verified = await readTeamConfig(sanitized, leaderCwd);
|
|
562
|
+
if (!verified || rollbackWorkers.some((worker) => !verified.workers.some((entry) => entry.name === worker.name && entry.pane_id === worker.pane_id))) {
|
|
563
|
+
throw new Error('canonical_scale_up_rollback_tracking_verification_failed');
|
|
564
|
+
}
|
|
565
|
+
Object.assign(config, verified);
|
|
268
566
|
});
|
|
269
567
|
}
|
|
270
|
-
catch {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
568
|
+
catch (trackingError) {
|
|
569
|
+
return { ok: false, error: `scale_up_rollback_membership_persistence_failed:${String(trackingError)}` };
|
|
570
|
+
}
|
|
571
|
+
const cleanupDebt = [];
|
|
572
|
+
try {
|
|
573
|
+
await removeDispatchRequestsForWorkers(sanitized, [...rollbackWorkerNames], leaderCwd);
|
|
574
|
+
}
|
|
575
|
+
catch (rollbackError) {
|
|
576
|
+
cleanupDebt.push(`authoritative_dispatch_cleanup_failed:${String(rollbackError)}`);
|
|
577
|
+
}
|
|
578
|
+
const rollbackPaneIds = [...new Set([
|
|
579
|
+
...rollbackWorkers.map((worker) => worker.pane_id),
|
|
580
|
+
context.paneId,
|
|
581
|
+
].filter((paneId) => typeof paneId === 'string' && paneId.trim().startsWith('%')))];
|
|
582
|
+
const resolvedPaneIds = new Set();
|
|
583
|
+
const unresolvedPaneIds = new Set();
|
|
584
|
+
const requiredTeamOwnerId = config.tmux_pane_owner_id?.trim();
|
|
585
|
+
const authorizedRollbackPaneIds = new Set();
|
|
586
|
+
const rollbackInstances = new Map();
|
|
587
|
+
for (const paneId of rollbackPaneIds) {
|
|
588
|
+
const matchingWorkers = rollbackWorkers.filter((worker) => worker.pane_id === paneId);
|
|
589
|
+
const worker = matchingWorkers.length === 1 ? matchingWorkers[0] : undefined;
|
|
590
|
+
const expectedPanePid = worker?.pid;
|
|
591
|
+
if (!worker || typeof expectedPanePid !== 'number' || !Number.isInteger(expectedPanePid) || expectedPanePid <= 0) {
|
|
592
|
+
unresolvedPaneIds.add(paneId);
|
|
593
|
+
cleanupDebt.push(`pane_pid_unpinned:${paneId}`);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (requiredTeamOwnerId && rollbackTaggedPaneOwnerIds.get(paneId) !== requiredTeamOwnerId) {
|
|
597
|
+
unresolvedPaneIds.add(paneId);
|
|
598
|
+
cleanupDebt.push(`pane_owner_unverified:${paneId}`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const expectedWindowId = worker.window_id;
|
|
602
|
+
const expectedSessionId = worker.session_id;
|
|
603
|
+
if (typeof expectedWindowId !== 'string' || !/^@[0-9]+$/.test(expectedWindowId)
|
|
604
|
+
|| typeof expectedSessionId !== 'string' || expectedSessionId.trim() === '') {
|
|
605
|
+
unresolvedPaneIds.add(paneId);
|
|
606
|
+
cleanupDebt.push(`pane_window_unpinned:${paneId}`);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const rollbackInstance = readScalePaneInstance(paneId);
|
|
610
|
+
if (!rollbackInstance
|
|
611
|
+
|| rollbackInstance.pid !== expectedPanePid
|
|
612
|
+
|| rollbackInstance.sessionName !== config.tmux_session
|
|
613
|
+
|| rollbackInstance.sessionId !== expectedSessionId
|
|
614
|
+
|| rollbackInstance.windowId !== expectedWindowId
|
|
615
|
+
|| (requiredTeamOwnerId && rollbackInstance.teamOwnerId !== requiredTeamOwnerId)) {
|
|
616
|
+
unresolvedPaneIds.add(paneId);
|
|
617
|
+
cleanupDebt.push(`pane_window_authority_lost:${paneId}`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
rollbackInstances.set(paneId, rollbackInstance);
|
|
621
|
+
authorizedRollbackPaneIds.add(paneId);
|
|
622
|
+
}
|
|
623
|
+
for (const paneId of authorizedRollbackPaneIds) {
|
|
624
|
+
const instance = rollbackInstances.get(paneId);
|
|
625
|
+
if (!instance || !killScalePaneWithAuthority(instance)) {
|
|
626
|
+
unresolvedPaneIds.add(paneId);
|
|
627
|
+
cleanupDebt.push(`pane_teardown_authority_lost:${paneId}`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
resolvedPaneIds.add(paneId);
|
|
631
|
+
}
|
|
632
|
+
const unresolvedWorkerNames = new Set(rollbackWorkers
|
|
633
|
+
.filter((worker) => typeof worker.pane_id === 'string' && unresolvedPaneIds.has(worker.pane_id))
|
|
634
|
+
.map((worker) => worker.name));
|
|
635
|
+
if (context.workerName && context.paneId && unresolvedPaneIds.has(context.paneId)) {
|
|
636
|
+
unresolvedWorkerNames.add(context.workerName);
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
|
|
640
|
+
await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
|
|
641
|
+
const currentConfigBytes = await readFile(configPath, 'utf8');
|
|
642
|
+
const currentConfig = JSON.parse(currentConfigBytes);
|
|
643
|
+
const currentManifestBytes = existsSync(manifestPath) ? await readFile(manifestPath, 'utf8') : null;
|
|
644
|
+
const retainedWorkers = rollbackWorkers.filter((worker) => unresolvedWorkerNames.has(worker.name));
|
|
645
|
+
const desiredByName = new Map(originalConfig.workers.map((worker) => [worker.name, worker]));
|
|
646
|
+
for (const worker of retainedWorkers)
|
|
647
|
+
desiredByName.set(worker.name, worker);
|
|
648
|
+
const desiredConfig = {
|
|
649
|
+
...originalConfig,
|
|
650
|
+
workers: [...desiredByName.values()],
|
|
651
|
+
worker_count: desiredByName.size,
|
|
652
|
+
next_worker_index: Math.max(originalConfig.next_worker_index ?? (originalConfig.workers.length + 1), ...retainedWorkers.map((worker) => worker.index + 1)),
|
|
653
|
+
};
|
|
654
|
+
const desiredManifestBytes = originalManifestBytes === null
|
|
655
|
+
? null
|
|
656
|
+
: JSON.stringify({
|
|
657
|
+
...JSON.parse(originalManifestBytes),
|
|
658
|
+
workers: desiredConfig.workers,
|
|
659
|
+
worker_count: desiredConfig.worker_count,
|
|
660
|
+
next_worker_index: desiredConfig.next_worker_index,
|
|
661
|
+
}, null, 2);
|
|
662
|
+
const taskChanges = [];
|
|
663
|
+
for (const taskId of createdTaskIds) {
|
|
664
|
+
const taskPath = join(teamStateRoot, 'team', sanitized, 'tasks', `task-${taskId}.json`);
|
|
665
|
+
const task = await readTask(sanitized, taskId, leaderCwd);
|
|
666
|
+
taskChanges.push({
|
|
667
|
+
taskId,
|
|
668
|
+
oldBytes: existsSync(taskPath) ? await readFile(taskPath, 'utf8') : null,
|
|
669
|
+
newBytes: task && unresolvedWorkerNames.has(task.owner ?? '')
|
|
670
|
+
? JSON.stringify(task, null, 2)
|
|
671
|
+
: null,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
await commitTeamMembershipTaskTransaction(sanitized, leaderCwd, {
|
|
675
|
+
baseGeneration: currentConfig.config_generation ?? 0,
|
|
676
|
+
tasks: taskChanges,
|
|
677
|
+
config: { oldBytes: currentConfigBytes, newBytes: JSON.stringify(desiredConfig, null, 2) },
|
|
678
|
+
manifest: { oldBytes: currentManifestBytes, newBytes: desiredManifestBytes },
|
|
679
|
+
recoverToNewOnFailure: true,
|
|
680
|
+
retainJournalOnSuccess: true,
|
|
681
|
+
failRollbackPersistence: hasScaleUpFailureInjection(env, 'rollback-membership-persistence'),
|
|
682
|
+
failRollbackPersistenceAfter: hasScaleUpFailureInjection(env, 'rollback-membership-config-persistence')
|
|
683
|
+
? 'config'
|
|
684
|
+
: hasScaleUpFailureInjection(env, 'rollback-membership-manifest-persistence')
|
|
685
|
+
? 'manifest'
|
|
686
|
+
: undefined,
|
|
687
|
+
});
|
|
688
|
+
const verified = JSON.parse(await readFile(configPath, 'utf8'));
|
|
689
|
+
if (!verified || retainedWorkers.some((worker) => !verified.workers.some((entry) => entry.name === worker.name && entry.pane_id === worker.pane_id))) {
|
|
690
|
+
throw new Error('canonical_scale_up_rollback_membership_verification_failed');
|
|
691
|
+
}
|
|
692
|
+
if (verified.workers.some((worker) => rollbackWorkerNames.has(worker.name) && !unresolvedWorkerNames.has(worker.name))) {
|
|
693
|
+
throw new Error('canonical_scale_up_rollback_resolved_membership_verification_failed');
|
|
694
|
+
}
|
|
695
|
+
await finalizeTeamMembershipTaskTransaction(sanitized, leaderCwd);
|
|
696
|
+
Object.assign(config, verified);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
catch (rollbackError) {
|
|
700
|
+
const journalPath = join(teamStateRoot, 'team', sanitized, '.membership-task-transaction.json');
|
|
701
|
+
const suffix = existsSync(journalPath) ? String(rollbackError) : `no_recoverable_journal:${String(rollbackError)}`;
|
|
702
|
+
return { ok: false, error: `scale_up_rollback_membership_persistence_failed:${suffix}` };
|
|
703
|
+
}
|
|
704
|
+
const cleanupWorkerNames = new Set([
|
|
705
|
+
...rollbackWorkers
|
|
706
|
+
.filter((worker) => typeof worker.pane_id !== 'string' || resolvedPaneIds.has(worker.pane_id))
|
|
707
|
+
.map((worker) => worker.name),
|
|
708
|
+
...(context.workerName && !unresolvedWorkerNames.has(context.workerName) ? [context.workerName] : []),
|
|
709
|
+
]);
|
|
710
|
+
try {
|
|
711
|
+
for (const taskId of createdTaskIds) {
|
|
712
|
+
const task = await readTask(sanitized, taskId, leaderCwd);
|
|
713
|
+
if (task && unresolvedWorkerNames.has(task.owner ?? ''))
|
|
714
|
+
continue;
|
|
715
|
+
await rm(join(teamStateRoot, 'team', sanitized, 'tasks', `task-${taskId}.json`), { force: true });
|
|
716
|
+
}
|
|
717
|
+
await Promise.all([...cleanupWorkerNames].map(async (workerName) => {
|
|
718
|
+
await rm(join(teamStateRoot, 'team', sanitized, 'workers', workerName), { recursive: true, force: true });
|
|
719
|
+
}));
|
|
720
|
+
for (const taskId of createdTaskIds) {
|
|
721
|
+
const task = await readTask(sanitized, taskId, leaderCwd);
|
|
722
|
+
if (task && unresolvedWorkerNames.has(task.owner ?? ''))
|
|
723
|
+
continue;
|
|
724
|
+
if (task)
|
|
725
|
+
throw new Error(`canonical_scale_up_rollback_task_verification_failed:${taskId}`);
|
|
726
|
+
}
|
|
727
|
+
for (const workerName of cleanupWorkerNames) {
|
|
728
|
+
if (existsSync(join(teamStateRoot, 'team', sanitized, 'workers', workerName))) {
|
|
729
|
+
throw new Error(`canonical_scale_up_rollback_worker_verification_failed:${workerName}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
catch (rollbackError) {
|
|
734
|
+
cleanupDebt.push(`canonical_cleanup_failed:${String(rollbackError)}`);
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
const contextWorkerName = context.worker?.name ?? context.workerName;
|
|
738
|
+
const contextWorktreePath = context.worker?.worktree_path ?? context.worktreePath;
|
|
739
|
+
if (contextWorkerName && contextWorktreePath && !unresolvedWorkerNames.has(contextWorkerName)) {
|
|
740
|
+
await removeWorkerWorktreeRootAgentsFile(sanitized, contextWorkerName, teamStateRoot, contextWorktreePath);
|
|
741
|
+
}
|
|
742
|
+
const unresolvedWorktreePaths = new Set(rollbackWorkers
|
|
743
|
+
.filter((worker) => unresolvedWorkerNames.has(worker.name) && typeof worker.worktree_path === 'string')
|
|
744
|
+
.map((worker) => resolve(worker.worktree_path)));
|
|
745
|
+
await rollbackProvisionedWorktrees(provisionedWorktrees.filter((worktree) => !unresolvedWorktreePaths.has(resolve(worktree.worktreePath))));
|
|
746
|
+
await Promise.all([...preparedWorkerDirectoryOwner.entries()]
|
|
747
|
+
.filter(([, workerName]) => !unresolvedWorkerNames.has(workerName))
|
|
748
|
+
.map(async ([workerDirPath]) => {
|
|
749
|
+
await rm(workerDirPath, { recursive: true, force: true });
|
|
750
|
+
}));
|
|
751
|
+
await Promise.all([...preparedStartupScriptOwner.entries()]
|
|
752
|
+
.filter(([, workerName]) => !unresolvedWorkerNames.has(workerName))
|
|
753
|
+
.map(async ([startupScriptPath]) => {
|
|
754
|
+
await rm(startupScriptPath, { force: true });
|
|
755
|
+
}));
|
|
756
|
+
if (!runtimeDirectoryExisted && unresolvedWorkerNames.size === 0)
|
|
757
|
+
await rm(runtimeDirectoryPath, { recursive: true, force: true });
|
|
758
|
+
}
|
|
759
|
+
catch (cleanupError) {
|
|
760
|
+
cleanupDebt.push(`resource_cleanup_failed:${String(cleanupError)}`);
|
|
761
|
+
}
|
|
762
|
+
if (cleanupDebt.length > 0) {
|
|
763
|
+
const reason = `scale_up_rollback_cleanup_debt:${cleanupDebt.join(';')}; retry cleanup for [${[...rollbackWorkerNames].join(',')}]`;
|
|
764
|
+
try {
|
|
765
|
+
await appendTeamEvent(sanitized, { type: 'team_leader_nudge', worker: 'leader-fixed', reason }, leaderCwd);
|
|
766
|
+
}
|
|
767
|
+
catch (eventError) {
|
|
768
|
+
cleanupDebt.push(`cleanup_debt_event_failed:${String(eventError)}`);
|
|
769
|
+
}
|
|
770
|
+
return { ok: false, error: `scale_up_rollback_cleanup_debt:${cleanupDebt.join(';')}` };
|
|
771
|
+
}
|
|
772
|
+
return { ok: false, error };
|
|
341
773
|
};
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const result = spawnSync('tmux', [
|
|
359
|
-
'split-window', splitDirection, '-t', splitTarget, '-d', '-P', '-F', '#{pane_id}', '-c', workerCwd, cmd,
|
|
360
|
-
], { encoding: 'utf-8' });
|
|
361
|
-
if (result.status !== 0) {
|
|
362
|
-
return await rollbackScaleUp(`Failed to create tmux pane for ${workerName}: ${(result.stderr || '').trim()}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
|
|
363
|
-
}
|
|
364
|
-
const paneId = (result.stdout || '').trim().split('\n')[0]?.trim();
|
|
365
|
-
if (!paneId || !paneId.startsWith('%')) {
|
|
366
|
-
return await rollbackScaleUp(`Failed to capture pane ID for ${workerName}`, {
|
|
367
|
-
paneId,
|
|
368
|
-
workerName,
|
|
369
|
-
worktreePath: workerWorkspace?.worktreePath,
|
|
370
|
-
});
|
|
774
|
+
// Persist incoming tasks only after launch policy is frozen; the resulting
|
|
775
|
+
// task listing is used for inbox and task materialization, never launch policy.
|
|
776
|
+
let materializedTasks;
|
|
777
|
+
try {
|
|
778
|
+
for (const task of tasks) {
|
|
779
|
+
const createdTask = await createStateTask(sanitized, {
|
|
780
|
+
subject: task.subject,
|
|
781
|
+
description: task.description,
|
|
782
|
+
status: 'pending',
|
|
783
|
+
owner: task.owner,
|
|
784
|
+
blocked_by: task.blocked_by,
|
|
785
|
+
role: task.role,
|
|
786
|
+
}, leaderCwd);
|
|
787
|
+
createdTaskIds.push(createdTask.id);
|
|
788
|
+
}
|
|
789
|
+
materializedTasks = await listTasks(sanitized, leaderCwd);
|
|
371
790
|
}
|
|
372
|
-
|
|
791
|
+
catch (error) {
|
|
792
|
+
return await rollbackScaleUp(`scale_up_task_materialization_failed:${error instanceof Error ? error.message : String(error)}`);
|
|
793
|
+
}
|
|
794
|
+
for (const workerLaunchPlan of workerLaunchPlans) {
|
|
795
|
+
const { workerIndex, workerName, runtimeRole, workerLaunchArgs, workerCli, } = workerLaunchPlan;
|
|
796
|
+
// Freeze the split target and its identity before preparing any artifacts.
|
|
797
|
+
// Later proof must establish that this exact pane process still owns the
|
|
798
|
+
// target; a recycled pane ID must never become split authority.
|
|
799
|
+
const splitTargetCandidate = config.workers[config.workers.length - 1];
|
|
800
|
+
const splitTargetWorker = splitTargetCandidate?.pane_id?.trim() ? splitTargetCandidate : undefined;
|
|
801
|
+
const splitTarget = splitTargetWorker?.pane_id ?? config.leader_pane_id ?? '';
|
|
802
|
+
const expectedSplitTargetPid = splitTargetWorker
|
|
803
|
+
? splitTargetWorker.pid
|
|
804
|
+
: config.leader_pane_pid;
|
|
805
|
+
const expectedSplitTargetOwnerId = typeof config.tmux_pane_owner_id === 'string'
|
|
806
|
+
? config.tmux_pane_owner_id.trim()
|
|
807
|
+
: '';
|
|
808
|
+
if (typeof expectedSplitTargetPid !== 'number'
|
|
809
|
+
|| !Number.isSafeInteger(expectedSplitTargetPid)
|
|
810
|
+
|| expectedSplitTargetPid <= 0) {
|
|
811
|
+
return await rollbackScaleUp(`scale_up_split_target_pid_missing:${splitTarget}`);
|
|
812
|
+
}
|
|
813
|
+
if (!expectedSplitTargetOwnerId) {
|
|
814
|
+
return await rollbackScaleUp(`scale_up_split_target_owner_unavailable:${splitTarget}`);
|
|
815
|
+
}
|
|
816
|
+
const splitDirection = splitTarget === (config.leader_pane_id ?? '') ? '-h' : '-v';
|
|
817
|
+
nextIndex = workerIndex + 1;
|
|
818
|
+
if (workerLaunchPlan.mixedTaskRoles.length > 1) {
|
|
819
|
+
console.log(`[omx:scaling] ${workerName}: mixed task roles [${workerLaunchPlan.mixedTaskRoles.join(', ')}], falling back to ${agentType}`);
|
|
820
|
+
}
|
|
821
|
+
// Prepare every pre-pane artifact under the rollback boundary. A pane is
|
|
822
|
+
// not required for ownership: workerName and any created worktree are
|
|
823
|
+
// sufficient for rollback to clean a failed preparation.
|
|
824
|
+
const workerDirPath = join(leaderCwd, '.omx', 'state', 'team', sanitized, 'workers', workerName);
|
|
825
|
+
const startupScriptPath = join(teamStateRoot, 'team', sanitized, 'runtime', `worker-${workerIndex}-startup.sh`);
|
|
826
|
+
let workerWorkspace = null;
|
|
827
|
+
let workerCwd = leaderCwd;
|
|
828
|
+
let cmd;
|
|
829
|
+
let rawRolePromptContent = null;
|
|
373
830
|
try {
|
|
374
|
-
|
|
831
|
+
preparedWorkerDirectoryOwner.set(workerDirPath, workerName);
|
|
832
|
+
await mkdir(workerDirPath, { recursive: true });
|
|
833
|
+
if (effectiveWorktreeMode.enabled) {
|
|
834
|
+
const worktreePlan = planWorktreeTarget({
|
|
835
|
+
cwd: leaderCwd,
|
|
836
|
+
scope: 'team',
|
|
837
|
+
mode: effectiveWorktreeMode,
|
|
838
|
+
teamName: sanitized,
|
|
839
|
+
workerName,
|
|
840
|
+
});
|
|
841
|
+
if (!worktreePlan.enabled)
|
|
842
|
+
throw new Error(`worktree_not_planned:${workerName}`);
|
|
843
|
+
const worktreePathExistedBeforeEnsure = existsSync(worktreePlan.worktreePath);
|
|
844
|
+
const branchExistedBeforeEnsure = worktreePlan.branchName
|
|
845
|
+
? spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${worktreePlan.branchName}`], {
|
|
846
|
+
cwd: worktreePlan.repoRoot,
|
|
847
|
+
encoding: 'utf-8',
|
|
848
|
+
windowsHide: true,
|
|
849
|
+
}).status === 0
|
|
850
|
+
: false;
|
|
851
|
+
try {
|
|
852
|
+
const ensuredWorkspace = ensureWorktree(worktreePlan);
|
|
853
|
+
throwIfScaleUpFailureInjected(env, 'worktree-ensure-post-create');
|
|
854
|
+
if (!ensuredWorkspace.enabled)
|
|
855
|
+
throw new Error(`worktree_not_provisioned:${workerName}`);
|
|
856
|
+
workerWorkspace = ensuredWorkspace;
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
const recoveredWorkspace = recoverCreatedWorktreeAfterEnsureFailure(worktreePlan, worktreePathExistedBeforeEnsure, branchExistedBeforeEnsure);
|
|
860
|
+
if (recoveredWorkspace) {
|
|
861
|
+
workerWorkspace = recoveredWorkspace;
|
|
862
|
+
provisionedWorktrees.push(recoveredWorkspace);
|
|
863
|
+
}
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
if (!workerWorkspace)
|
|
867
|
+
throw new Error(`worktree_not_provisioned:${workerName}`);
|
|
868
|
+
provisionedWorktrees.push(workerWorkspace);
|
|
869
|
+
throwIfScaleUpFailureInjected(env, 'worktree-post-create');
|
|
870
|
+
workerCwd = workerWorkspace.worktreePath;
|
|
871
|
+
}
|
|
872
|
+
rawRolePromptContent = await loadRolePrompt(runtimeRole, join(leaderCwd, '.codex', 'prompts'))
|
|
873
|
+
?? await loadRolePrompt(runtimeRole, codexPromptsDir());
|
|
874
|
+
const resolvedWorkerModel = parseTeamWorkerLaunchArgs(workerLaunchArgs).modelOverride ?? undefined;
|
|
875
|
+
const rolePromptContent = rawRolePromptContent
|
|
876
|
+
? composeRoleInstructionsForRole(runtimeRole, rawRolePromptContent, resolvedWorkerModel)
|
|
877
|
+
: null;
|
|
878
|
+
const teamInstructionsPath = join(leaderCwd, '.omx', 'state', 'team', sanitized, 'worker-agents.md');
|
|
879
|
+
const instructionsFilePath = workerWorkspace
|
|
880
|
+
? await writeWorkerWorktreeRootAgentsFile({
|
|
881
|
+
teamName: sanitized,
|
|
882
|
+
workerName,
|
|
883
|
+
workerRole: runtimeRole,
|
|
884
|
+
rolePromptContent: rolePromptContent ?? '',
|
|
885
|
+
teamStateRoot,
|
|
886
|
+
leaderCwd,
|
|
887
|
+
worktreePath: workerWorkspace.worktreePath,
|
|
888
|
+
})
|
|
889
|
+
: rolePromptContent
|
|
890
|
+
? await writeWorkerRoleInstructionsFile(sanitized, workerName, leaderCwd, teamInstructionsPath, runtimeRole, rolePromptContent)
|
|
891
|
+
: teamInstructionsPath;
|
|
892
|
+
const extraEnv = {
|
|
893
|
+
OMX_TEAM_STATE_ROOT: teamStateRoot,
|
|
894
|
+
OMX_TEAM_LEADER_CWD: leaderCwd,
|
|
895
|
+
OMX_MODEL_INSTRUCTIONS_FILE: instructionsFilePath,
|
|
896
|
+
...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
|
|
897
|
+
};
|
|
898
|
+
if (workerWorkspace) {
|
|
899
|
+
extraEnv.OMX_TEAM_WORKTREE_PATH = workerWorkspace.worktreePath;
|
|
900
|
+
if (workerWorkspace.branchName) {
|
|
901
|
+
extraEnv.OMX_TEAM_WORKTREE_BRANCH = workerWorkspace.branchName;
|
|
902
|
+
}
|
|
903
|
+
extraEnv.OMX_TEAM_WORKTREE_DETACHED = workerWorkspace.detached ? '1' : '0';
|
|
904
|
+
}
|
|
905
|
+
trustWorkerMiseConfigIfAvailable(workerCwd);
|
|
906
|
+
preparedStartupScriptOwner.set(startupScriptPath, workerName);
|
|
907
|
+
const startupCommand = writeWorkerStartupScriptCommand(sanitized, workerIndex, workerLaunchArgs, workerCwd, extraEnv, workerCli, undefined, runtimeRole);
|
|
908
|
+
cmd = startupCommand ?? buildWorkerStartupCommand(sanitized, workerIndex, workerLaunchArgs, workerCwd, extraEnv, workerCli, undefined, runtimeRole);
|
|
375
909
|
}
|
|
376
910
|
catch (error) {
|
|
377
|
-
return await rollbackScaleUp(`
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// Intentionally avoid forcing `select-layout tiled` here.
|
|
381
|
-
// Tiled relayout reflows leader/HUD panes and breaks team window layout.
|
|
382
|
-
// Get PID
|
|
383
|
-
const panePid = getWorkerPanePid(sessionName, workerIndex, paneId);
|
|
384
|
-
const workerInfo = {
|
|
385
|
-
name: workerName,
|
|
386
|
-
index: workerIndex,
|
|
387
|
-
role: runtimeRole,
|
|
388
|
-
worker_cli: workerCli,
|
|
389
|
-
assigned_tasks: [],
|
|
390
|
-
pid: panePid ?? undefined,
|
|
391
|
-
pane_id: paneId,
|
|
392
|
-
working_dir: workerCwd,
|
|
393
|
-
worktree_repo_root: workerWorkspace ? workerWorkspace.repoRoot : undefined,
|
|
394
|
-
worktree_path: workerWorkspace ? workerWorkspace.worktreePath : undefined,
|
|
395
|
-
worktree_branch: workerWorkspace ? (workerWorkspace.branchName ?? undefined) : undefined,
|
|
396
|
-
worktree_detached: workerWorkspace ? workerWorkspace.detached : undefined,
|
|
397
|
-
worktree_created: workerWorkspace ? workerWorkspace.created : undefined,
|
|
398
|
-
team_state_root: teamStateRoot,
|
|
399
|
-
};
|
|
400
|
-
await writeWorkerIdentity(sanitized, workerName, workerInfo, leaderCwd);
|
|
401
|
-
// Wait for worker readiness
|
|
402
|
-
const readyTimeoutMs = resolveWorkerReadyTimeoutMs(env);
|
|
403
|
-
const skipReadyWait = env.OMX_TEAM_SKIP_READY_WAIT === '1';
|
|
404
|
-
if (!skipReadyWait) {
|
|
405
|
-
const ready = waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId);
|
|
406
|
-
if (!ready) {
|
|
407
|
-
console.log(`[omx:scaling] Warning: worker ${workerName} did not become ready within timeout`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
// Get assigned tasks for this worker
|
|
411
|
-
const workerTasks = materializedTasks.filter(t => t.owner === workerName);
|
|
412
|
-
const inbox = generateInitialInbox(workerName, sanitized, agentType, workerTasks, {
|
|
413
|
-
teamStateRoot,
|
|
414
|
-
leaderCwd,
|
|
415
|
-
workerRole: runtimeRole,
|
|
416
|
-
rolePromptContent: rawRolePromptContent ?? undefined,
|
|
417
|
-
worktreeRootAgentsCanonical: Boolean(workerWorkspace?.worktreePath),
|
|
418
|
-
approvedContextSection,
|
|
419
|
-
workerGoalInstruction: buildTeamWorkerGoalInstruction(sanitized, workerName, workerTasks, { teamStateRoot }),
|
|
420
|
-
});
|
|
421
|
-
const triggerDirective = buildTriggerDirective(workerName, sanitized, resolveInstructionStateRoot(workerInfo.worktree_path));
|
|
422
|
-
const queued = await queueInboxInstruction({
|
|
423
|
-
teamName: sanitized,
|
|
424
|
-
workerName,
|
|
425
|
-
workerIndex,
|
|
426
|
-
paneId,
|
|
427
|
-
inbox,
|
|
428
|
-
triggerMessage: triggerDirective.text,
|
|
429
|
-
intent: triggerDirective.intent,
|
|
430
|
-
cwd: leaderCwd,
|
|
431
|
-
transportPreference: dispatchPolicy.dispatch_mode,
|
|
432
|
-
fallbackAllowed: true,
|
|
433
|
-
inboxCorrelationKey: `scale_up:${workerName}`,
|
|
434
|
-
notify: async (_target, message) => {
|
|
435
|
-
if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback') {
|
|
436
|
-
return { ok: true, transport: 'hook', reason: 'queued_for_hook_dispatch' };
|
|
437
|
-
}
|
|
438
|
-
return await notifyWorkerPaneOutcome(sessionName, workerIndex, message, paneId, workerCli);
|
|
439
|
-
},
|
|
440
|
-
});
|
|
441
|
-
let outcome = queued;
|
|
442
|
-
if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback' && queued.request_id) {
|
|
443
|
-
const receipt = await waitForDispatchReceipt(sanitized, queued.request_id, leaderCwd, {
|
|
444
|
-
timeoutMs: dispatchPolicy.dispatch_ack_timeout_ms,
|
|
445
|
-
pollMs: 50,
|
|
446
|
-
});
|
|
447
|
-
if (receipt && (receipt.status === 'notified' || receipt.status === 'delivered')) {
|
|
448
|
-
outcome = { ok: true, transport: 'hook', reason: `hook_receipt_${receipt.status}`, request_id: queued.request_id };
|
|
911
|
+
return await rollbackScaleUp(`scale_up_worker_preparation_failed:${workerName}:${error instanceof Error ? error.message : String(error)}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
|
|
449
912
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
913
|
+
// Freeze and validate the full source-pane incarnation. The server owns
|
|
914
|
+
// the final comparison and split, so a recycled pane ID cannot become a
|
|
915
|
+
// split target between client-side proof and the topology mutation.
|
|
916
|
+
const splitTargetProof = readExactPaneProofSync(splitTarget);
|
|
917
|
+
const splitTargetInstance = splitTargetProof.status === 'live'
|
|
918
|
+
? readScalePaneInstance(splitTargetProof.paneId)
|
|
919
|
+
: null;
|
|
920
|
+
if (!splitTargetInstance) {
|
|
921
|
+
return await rollbackScaleUp(`scale_up_split_target_proof_unavailable:${splitTarget}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
|
|
922
|
+
}
|
|
923
|
+
if (splitTargetInstance.pid !== expectedSplitTargetPid) {
|
|
924
|
+
return await rollbackScaleUp(`scale_up_split_target_pid_changed:${splitTarget}:${expectedSplitTargetPid}:${splitTargetInstance.pid}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
|
|
925
|
+
}
|
|
926
|
+
if (splitTargetInstance.sessionName !== sessionName
|
|
927
|
+
|| (config.tmux_session_id && splitTargetInstance.sessionId !== config.tmux_session_id)
|
|
928
|
+
|| splitTargetInstance.teamOwnerId !== expectedSplitTargetOwnerId) {
|
|
929
|
+
return await rollbackScaleUp(`scale_up_split_target_authority_changed:${splitTargetInstance.paneId}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
|
|
930
|
+
}
|
|
931
|
+
const createdPane = splitWorkerPaneWithScaleAuthority(splitTargetInstance, splitDirection, workerCwd, cmd);
|
|
932
|
+
if (!createdPane) {
|
|
933
|
+
return await rollbackScaleUp(`scale_up_split_target_authority_lost:${splitTargetInstance.paneId}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
|
|
934
|
+
}
|
|
935
|
+
const paneId = createdPane.paneId;
|
|
936
|
+
const workerInfo = {
|
|
937
|
+
name: workerName,
|
|
938
|
+
index: workerIndex,
|
|
939
|
+
role: runtimeRole,
|
|
940
|
+
worker_cli: workerCli,
|
|
941
|
+
assigned_tasks: [],
|
|
942
|
+
pid: createdPane.pid,
|
|
943
|
+
pane_id: paneId,
|
|
944
|
+
session_id: createdPane.sessionId,
|
|
945
|
+
window_id: createdPane.windowId,
|
|
946
|
+
working_dir: workerCwd,
|
|
947
|
+
worktree_repo_root: workerWorkspace ? workerWorkspace.repoRoot : undefined,
|
|
948
|
+
worktree_path: workerWorkspace ? workerWorkspace.worktreePath : undefined,
|
|
949
|
+
worktree_branch: workerWorkspace ? (workerWorkspace.branchName ?? undefined) : undefined,
|
|
950
|
+
worktree_detached: workerWorkspace ? workerWorkspace.detached : undefined,
|
|
951
|
+
worktree_created: workerWorkspace ? workerWorkspace.created : undefined,
|
|
952
|
+
team_state_root: teamStateRoot,
|
|
953
|
+
};
|
|
954
|
+
const taggedPane = config.tmux_pane_owner_id
|
|
955
|
+
? tagScalePaneWithAuthority(createdPane, config.tmux_pane_owner_id.trim())
|
|
956
|
+
: null;
|
|
957
|
+
if (!taggedPane) {
|
|
958
|
+
return await rollbackScaleUp(`scale_up_created_pane_owner_tag_authority_lost:${paneId}`, { paneId, worker: workerInfo });
|
|
959
|
+
}
|
|
960
|
+
rollbackTaggedPaneOwnerIds.set(paneId, taggedPane.teamOwnerId);
|
|
961
|
+
// Intentionally avoid forcing `select-layout tiled` here.
|
|
962
|
+
// Tiled relayout reflows leader/HUD panes and breaks team window layout.
|
|
963
|
+
try {
|
|
964
|
+
await writeWorkerIdentity(sanitized, workerName, workerInfo, leaderCwd);
|
|
965
|
+
throwIfScaleUpFailureInjected(env, 'identity');
|
|
966
|
+
// Wait for worker readiness
|
|
967
|
+
const readyTimeoutMs = resolveWorkerReadyTimeoutMs(env);
|
|
968
|
+
const skipReadyWait = env.OMX_TEAM_SKIP_READY_WAIT === '1';
|
|
969
|
+
if (!skipReadyWait) {
|
|
970
|
+
const ready = waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
971
|
+
if (!ready) {
|
|
972
|
+
console.log(`[omx:scaling] Warning: worker ${workerName} did not become ready within timeout`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
// Get assigned tasks for this worker
|
|
976
|
+
const workerTasks = materializedTasks.filter(t => t.owner === workerName);
|
|
977
|
+
const inbox = generateInitialInbox(workerName, sanitized, agentType, workerTasks, {
|
|
978
|
+
teamStateRoot,
|
|
979
|
+
leaderCwd,
|
|
980
|
+
workerRole: runtimeRole,
|
|
981
|
+
rolePromptContent: rawRolePromptContent ?? undefined,
|
|
982
|
+
worktreeRootAgentsCanonical: Boolean(workerWorkspace?.worktreePath),
|
|
983
|
+
approvedContextSection,
|
|
984
|
+
workerGoalInstruction: buildTeamWorkerGoalInstruction(sanitized, workerName, workerTasks, { teamStateRoot }),
|
|
985
|
+
});
|
|
986
|
+
throwIfScaleUpFailureInjected(env, 'inbox');
|
|
987
|
+
const triggerDirective = buildTriggerDirective(workerName, sanitized, resolveInstructionStateRoot(workerInfo.worktree_path));
|
|
988
|
+
const queued = await queueInboxInstruction({
|
|
989
|
+
teamName: sanitized,
|
|
990
|
+
workerName,
|
|
991
|
+
workerIndex,
|
|
992
|
+
paneId,
|
|
993
|
+
inbox,
|
|
994
|
+
triggerMessage: triggerDirective.text,
|
|
995
|
+
intent: triggerDirective.intent,
|
|
996
|
+
cwd: leaderCwd,
|
|
997
|
+
transportPreference: dispatchPolicy.dispatch_mode,
|
|
998
|
+
fallbackAllowed: true,
|
|
999
|
+
inboxCorrelationKey: `scale_up:${workerName}`,
|
|
1000
|
+
notify: async (_target, message) => {
|
|
1001
|
+
if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback') {
|
|
1002
|
+
return { ok: true, transport: 'hook', reason: 'queued_for_hook_dispatch' };
|
|
1003
|
+
}
|
|
1004
|
+
return await notifyWorkerPaneOutcome(sessionName, message, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, workerInfo.session_id, workerInfo.window_id);
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
let outcome = queued;
|
|
1008
|
+
if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback' && queued.request_id) {
|
|
1009
|
+
const receipt = await waitForDispatchReceipt(sanitized, queued.request_id, leaderCwd, {
|
|
1010
|
+
timeoutMs: dispatchPolicy.dispatch_ack_timeout_ms,
|
|
1011
|
+
pollMs: 50,
|
|
1012
|
+
});
|
|
1013
|
+
if (receipt && (receipt.status === 'notified' || receipt.status === 'delivered')) {
|
|
1014
|
+
outcome = { ok: true, transport: 'hook', reason: `hook_receipt_${receipt.status}`, request_id: queued.request_id };
|
|
461
1015
|
}
|
|
462
1016
|
else {
|
|
463
|
-
await
|
|
464
|
-
|
|
465
|
-
ok
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1017
|
+
const fallback = await notifyWorkerPaneOutcome(sessionName, triggerDirective.text, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, workerInfo.session_id, workerInfo.window_id);
|
|
1018
|
+
if (receipt?.status === 'failed') {
|
|
1019
|
+
if (fallback.ok) {
|
|
1020
|
+
await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, leaderCwd).catch(() => { });
|
|
1021
|
+
outcome = {
|
|
1022
|
+
ok: true,
|
|
1023
|
+
transport: fallback.transport,
|
|
1024
|
+
reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}`,
|
|
1025
|
+
request_id: queued.request_id,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` }, leaderCwd).catch(() => { });
|
|
1030
|
+
outcome = {
|
|
1031
|
+
ok: false,
|
|
1032
|
+
transport: fallback.transport,
|
|
1033
|
+
reason: `fallback_attempted_but_unconfirmed:${fallback.reason}`,
|
|
1034
|
+
request_id: queued.request_id,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
else if (fallback.ok) {
|
|
1039
|
+
const marked = await markDispatchRequestNotified(sanitized, queued.request_id, { last_reason: `fallback_confirmed:${fallback.reason}` }, leaderCwd);
|
|
1040
|
+
if (!marked) {
|
|
1041
|
+
await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, leaderCwd).catch(() => { });
|
|
1042
|
+
}
|
|
1043
|
+
outcome = {
|
|
1044
|
+
ok: true,
|
|
1045
|
+
transport: fallback.transport,
|
|
1046
|
+
reason: `hook_timeout_fallback_confirmed:${fallback.reason}`,
|
|
1047
|
+
request_id: queued.request_id,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
const current = await readDispatchRequest(sanitized, queued.request_id, leaderCwd);
|
|
1052
|
+
if (current) {
|
|
1053
|
+
await transitionDispatchRequest(sanitized, queued.request_id, current.status, 'failed', { last_reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` }, leaderCwd).catch(() => { });
|
|
1054
|
+
}
|
|
1055
|
+
outcome = {
|
|
1056
|
+
ok: false,
|
|
1057
|
+
transport: fallback.transport,
|
|
1058
|
+
reason: `fallback_attempted_but_unconfirmed:${fallback.reason}`,
|
|
1059
|
+
request_id: queued.request_id,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
470
1062
|
}
|
|
471
1063
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
1064
|
+
// Retry dispatch once if a trust prompt is blocking the worker pane (fixes #393).
|
|
1065
|
+
if (!outcome.ok && dismissTrustPromptIfPresent(sessionName, workerIndex, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
|
|
1066
|
+
waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
1067
|
+
const retry = await notifyWorkerPaneOutcome(sessionName, triggerDirective.text, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, workerInfo.session_id, workerInfo.window_id);
|
|
1068
|
+
if (retry.ok) {
|
|
1069
|
+
outcome = retry;
|
|
476
1070
|
}
|
|
477
|
-
outcome = {
|
|
478
|
-
ok: true,
|
|
479
|
-
transport: fallback.transport,
|
|
480
|
-
reason: `hook_timeout_fallback_confirmed:${fallback.reason}`,
|
|
481
|
-
request_id: queued.request_id,
|
|
482
|
-
};
|
|
483
1071
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
outcome = {
|
|
490
|
-
ok: false,
|
|
491
|
-
transport: fallback.transport,
|
|
492
|
-
reason: `fallback_attempted_but_unconfirmed:${fallback.reason}`,
|
|
493
|
-
request_id: queued.request_id,
|
|
494
|
-
};
|
|
1072
|
+
if (!outcome.ok) {
|
|
1073
|
+
return await rollbackScaleUp(`scale_up_dispatch_failed:${workerName}:${outcome.reason}`, {
|
|
1074
|
+
paneId,
|
|
1075
|
+
worker: workerInfo,
|
|
1076
|
+
});
|
|
495
1077
|
}
|
|
1078
|
+
throwIfScaleUpFailureInjected(env, 'post-dispatch-rollback');
|
|
1079
|
+
addedWorkers.push(workerInfo);
|
|
1080
|
+
config.workers.push(workerInfo);
|
|
1081
|
+
config.worker_count = config.workers.length;
|
|
1082
|
+
config.next_worker_index = nextIndex;
|
|
1083
|
+
throwIfScaleUpFailureInjected(env, 'config');
|
|
1084
|
+
await saveTeamConfig(config, leaderCwd);
|
|
1085
|
+
throwIfScaleUpFailureInjected(env, 'finalization');
|
|
496
1086
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (!outcome.ok && dismissTrustPromptIfPresent(sessionName, workerIndex, paneId)) {
|
|
500
|
-
waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId);
|
|
501
|
-
const retry = await notifyWorkerPaneOutcome(sessionName, workerIndex, triggerDirective.text, paneId, workerCli);
|
|
502
|
-
if (retry.ok) {
|
|
503
|
-
outcome = retry;
|
|
1087
|
+
catch (error) {
|
|
1088
|
+
return await rollbackScaleUp(`scale_up_worker_materialization_failed:${workerName}:${error instanceof Error ? error.message : String(error)}`, { paneId, worker: workerInfo });
|
|
504
1089
|
}
|
|
505
1090
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
});
|
|
1091
|
+
try {
|
|
1092
|
+
await appendTeamEvent(sanitized, {
|
|
1093
|
+
type: 'team_leader_nudge',
|
|
1094
|
+
worker: 'leader-fixed',
|
|
1095
|
+
reason: `scale_up: added ${count} worker(s), new count=${config.worker_count}`,
|
|
1096
|
+
}, leaderCwd);
|
|
512
1097
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
return await rollbackScaleUp(`scale_up_finalization_failed:${error instanceof Error ? error.message : String(error)}`);
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
ok: true,
|
|
1103
|
+
addedWorkers,
|
|
1104
|
+
newWorkerCount: config.worker_count,
|
|
1105
|
+
nextWorkerIndex: nextIndex,
|
|
1106
|
+
};
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
export function isSameOrInsidePath(path, root, pathSemantics = { relative, isAbsolute, sep }) {
|
|
1111
|
+
const relativePath = pathSemantics.relative(root, path);
|
|
1112
|
+
return relativePath === ''
|
|
1113
|
+
|| (!pathSemantics.isAbsolute(relativePath)
|
|
1114
|
+
&& relativePath !== '..'
|
|
1115
|
+
&& !relativePath.startsWith(`..${pathSemantics.sep}`));
|
|
1116
|
+
}
|
|
1117
|
+
async function assertExistingPathParentContained(path, root) {
|
|
1118
|
+
const canonicalRoot = await realpath(root);
|
|
1119
|
+
let candidate = resolve(path);
|
|
1120
|
+
while (!existsSync(candidate)) {
|
|
1121
|
+
const parent = dirname(candidate);
|
|
1122
|
+
if (parent === candidate)
|
|
1123
|
+
throw new Error(`scale_down_cleanup_debt_path_missing_parent:${path}`);
|
|
1124
|
+
candidate = parent;
|
|
1125
|
+
}
|
|
1126
|
+
const canonicalExistingPath = await realpath(candidate);
|
|
1127
|
+
if (!isSameOrInsidePath(canonicalExistingPath, canonicalRoot)) {
|
|
1128
|
+
throw new Error(`scale_down_cleanup_debt_path_escape:${path}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function validateScaleDownCleanupResources(teamName, leaderCwd, teamStateRoot, workers) {
|
|
1132
|
+
const canonicalLeaderCwd = await realpath(leaderCwd);
|
|
1133
|
+
let repoRoot = canonicalLeaderCwd;
|
|
1134
|
+
const repoRootResult = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
1135
|
+
cwd: leaderCwd,
|
|
1136
|
+
encoding: 'utf-8',
|
|
1137
|
+
windowsHide: true,
|
|
1138
|
+
});
|
|
1139
|
+
const reportedRepoRoot = (repoRootResult.stdout || '').trim();
|
|
1140
|
+
if (repoRootResult.status === 0 && reportedRepoRoot) {
|
|
1141
|
+
repoRoot = await realpath(resolve(reportedRepoRoot));
|
|
1142
|
+
}
|
|
1143
|
+
const canonicalTeamStateRoot = await realpath(resolve(teamStateRoot));
|
|
1144
|
+
const expectedWorkerRoot = join(canonicalTeamStateRoot, 'team', teamName, 'workers');
|
|
1145
|
+
for (const worker of workers) {
|
|
1146
|
+
const expectedWorkerDirectory = join(expectedWorkerRoot, worker.name);
|
|
1147
|
+
await assertExistingPathParentContained(expectedWorkerDirectory, canonicalTeamStateRoot);
|
|
1148
|
+
if (worker.team_state_root !== undefined
|
|
1149
|
+
&& (!isAbsolute(worker.team_state_root) || worker.team_state_root !== resolve(worker.team_state_root)
|
|
1150
|
+
|| resolve(worker.team_state_root) !== canonicalTeamStateRoot)) {
|
|
1151
|
+
throw new Error(`scale_down_cleanup_debt_invalid_team_state_root:${worker.name}`);
|
|
1152
|
+
}
|
|
1153
|
+
if (worker.worktree_path === undefined) {
|
|
1154
|
+
if (worker.worktree_repo_root !== undefined || worker.worktree_branch !== undefined
|
|
1155
|
+
|| worker.worktree_detached !== undefined || worker.worktree_created !== undefined) {
|
|
1156
|
+
throw new Error(`scale_down_cleanup_debt_invalid_worktree_metadata:${worker.name}`);
|
|
1157
|
+
}
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
const expectedWorktreePath = join(repoRoot, '.omx', 'team', teamName, 'worktrees', worker.name);
|
|
1161
|
+
if (!isAbsolute(worker.worktree_path) || worker.worktree_path !== resolve(worker.worktree_path)
|
|
1162
|
+
|| resolve(worker.worktree_path) !== expectedWorktreePath
|
|
1163
|
+
|| (worker.worktree_repo_root !== undefined && (!isAbsolute(worker.worktree_repo_root)
|
|
1164
|
+
|| worker.worktree_repo_root !== resolve(worker.worktree_repo_root)
|
|
1165
|
+
|| resolve(worker.worktree_repo_root) !== repoRoot))
|
|
1166
|
+
|| (worker.worktree_detached !== undefined && typeof worker.worktree_detached !== 'boolean')
|
|
1167
|
+
|| (worker.worktree_created !== undefined && typeof worker.worktree_created !== 'boolean')
|
|
1168
|
+
|| (worker.worktree_created === true && (typeof worker.worktree_repo_root !== 'string' || typeof worker.worktree_detached !== 'boolean'))) {
|
|
1169
|
+
throw new Error(`scale_down_cleanup_debt_invalid_worktree_target:${worker.name}`);
|
|
1170
|
+
}
|
|
1171
|
+
await assertExistingPathParentContained(expectedWorktreePath, repoRoot);
|
|
1172
|
+
if (existsSync(expectedWorktreePath) && await realpath(expectedWorktreePath) !== expectedWorktreePath) {
|
|
1173
|
+
throw new Error(`scale_down_cleanup_debt_worktree_symlink:${worker.name}`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async function cleanupScaleDownResources(teamName, teamStateRoot, workers) {
|
|
1178
|
+
for (const worker of workers) {
|
|
1179
|
+
// An absent exact worktree means a prior cleanup completed before crashing.
|
|
1180
|
+
// Do not replay a restoration or git removal against a replacement path.
|
|
1181
|
+
if (worker.worktree_path && existsSync(worker.worktree_path)) {
|
|
1182
|
+
await removeWorkerWorktreeRootAgentsFile(teamName, worker.name, worker.team_state_root ?? teamStateRoot, worker.worktree_path);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const worktrees = workers
|
|
1186
|
+
.filter((worker) => worker.worktree_created === true
|
|
1187
|
+
&& typeof worker.worktree_path === 'string'
|
|
1188
|
+
&& existsSync(worker.worktree_path)
|
|
1189
|
+
&& typeof worker.worktree_repo_root === 'string'
|
|
1190
|
+
&& typeof worker.worktree_detached === 'boolean')
|
|
1191
|
+
.map((worker) => ({
|
|
1192
|
+
enabled: true,
|
|
1193
|
+
repoRoot: worker.worktree_repo_root,
|
|
1194
|
+
worktreePath: worker.worktree_path,
|
|
1195
|
+
detached: worker.worktree_detached,
|
|
1196
|
+
branchName: worker.worktree_branch ?? null,
|
|
1197
|
+
created: true,
|
|
1198
|
+
reused: false,
|
|
1199
|
+
// Membership records do not establish branch ownership. Preserve a named
|
|
1200
|
+
// branch rather than deleting one that may have predated this worker.
|
|
1201
|
+
createdBranch: false,
|
|
1202
|
+
}));
|
|
1203
|
+
if (worktrees.length > 0)
|
|
1204
|
+
await rollbackProvisionedWorktrees(worktrees);
|
|
1205
|
+
await Promise.all(workers.map(async (worker) => {
|
|
1206
|
+
await rm(join(teamStateRoot, 'team', teamName, 'workers', worker.name), { recursive: true, force: true });
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
function asScaleDownDebtResources(debt, removedWorkerNames) {
|
|
1210
|
+
const resources = debt.resource_workers;
|
|
1211
|
+
if (!Array.isArray(resources) || resources.length !== removedWorkerNames.length)
|
|
1212
|
+
return null;
|
|
1213
|
+
const expected = new Set(removedWorkerNames);
|
|
1214
|
+
const seen = new Set();
|
|
1215
|
+
for (const worker of resources) {
|
|
1216
|
+
if (!worker || typeof worker.name !== 'string' || !expected.has(worker.name) || seen.has(worker.name)
|
|
1217
|
+
|| (worker.worktree_path !== undefined && typeof worker.worktree_path !== 'string')
|
|
1218
|
+
|| (worker.worktree_repo_root !== undefined && typeof worker.worktree_repo_root !== 'string')
|
|
1219
|
+
|| (worker.worktree_branch !== undefined && typeof worker.worktree_branch !== 'string')
|
|
1220
|
+
|| (worker.worktree_detached !== undefined && typeof worker.worktree_detached !== 'boolean')
|
|
1221
|
+
|| (worker.worktree_created !== undefined && typeof worker.worktree_created !== 'boolean')
|
|
1222
|
+
|| (worker.team_state_root !== undefined && typeof worker.team_state_root !== 'string'))
|
|
1223
|
+
return null;
|
|
1224
|
+
seen.add(worker.name);
|
|
1225
|
+
}
|
|
1226
|
+
return resources;
|
|
1227
|
+
}
|
|
1228
|
+
function asScaleDownDebtPanes(debt, removedWorkerNames, resources, leaderPaneId, hudPaneId) {
|
|
1229
|
+
const workers = debt.workers;
|
|
1230
|
+
const unresolved = debt.unresolved_panes;
|
|
1231
|
+
if (!Array.isArray(workers) || (unresolved !== undefined && !Array.isArray(unresolved)))
|
|
1232
|
+
return null;
|
|
1233
|
+
const expectedNames = new Set(removedWorkerNames);
|
|
1234
|
+
const resourceNames = new Set(resources.map((resource) => resource.name));
|
|
1235
|
+
const canonicalByName = new Map();
|
|
1236
|
+
const allPaneIds = new Set();
|
|
1237
|
+
const normalizedLeaderPaneId = typeof leaderPaneId === 'string' ? leaderPaneId.trim() : '';
|
|
1238
|
+
const normalizedHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
|
|
1239
|
+
const validate = (pane, requireCanonicalMatch) => {
|
|
1240
|
+
if (!pane || typeof pane.name !== 'string' || !expectedNames.has(pane.name) || !resourceNames.has(pane.name)
|
|
1241
|
+
|| typeof pane.index !== 'number' || !Number.isInteger(pane.index) || pane.index <= 0
|
|
1242
|
+
|| pane.index !== Number(pane.name.slice('worker-'.length))
|
|
1243
|
+
|| typeof pane.pane_id !== 'string' || !/^%\d+$/.test(pane.pane_id)
|
|
1244
|
+
|| pane.pane_id === normalizedLeaderPaneId || pane.pane_id === normalizedHudPaneId
|
|
1245
|
+
|| (pane.pid !== null && (!Number.isSafeInteger(pane.pid) || pane.pid <= 0)))
|
|
1246
|
+
return false;
|
|
1247
|
+
const canonical = canonicalByName.get(pane.name);
|
|
1248
|
+
if (requireCanonicalMatch) {
|
|
1249
|
+
return canonical !== undefined
|
|
1250
|
+
&& canonical.index === pane.index
|
|
1251
|
+
&& canonical.pane_id === pane.pane_id
|
|
1252
|
+
&& canonical.pid === pane.pid;
|
|
1253
|
+
}
|
|
1254
|
+
if (canonical !== undefined || allPaneIds.has(pane.pane_id))
|
|
1255
|
+
return false;
|
|
1256
|
+
canonicalByName.set(pane.name, pane);
|
|
1257
|
+
allPaneIds.add(pane.pane_id);
|
|
1258
|
+
return true;
|
|
1259
|
+
};
|
|
1260
|
+
for (const worker of workers) {
|
|
1261
|
+
if (!validate(worker, false))
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
const unresolvedNames = new Set();
|
|
1265
|
+
const unresolvedPaneIds = new Set();
|
|
1266
|
+
for (const pane of unresolved ?? []) {
|
|
1267
|
+
if (unresolvedNames.has(pane.name ?? '') || unresolvedPaneIds.has(pane.pane_id)
|
|
1268
|
+
|| !validate(pane, true))
|
|
1269
|
+
return null;
|
|
1270
|
+
unresolvedNames.add(pane.name);
|
|
1271
|
+
unresolvedPaneIds.add(pane.pane_id);
|
|
1272
|
+
}
|
|
1273
|
+
return unresolved && unresolved.length > 0 ? unresolved : workers;
|
|
1274
|
+
}
|
|
1275
|
+
async function hasMatchingScaleDownDebtWorkerIdentity(teamStateRoot, teamName, pane) {
|
|
1276
|
+
try {
|
|
1277
|
+
const identity = JSON.parse(await readFile(join(teamStateRoot, 'team', teamName, 'workers', pane.name ?? '', 'identity.json'), 'utf8'));
|
|
1278
|
+
return identity.name === pane.name
|
|
1279
|
+
&& identity.index === pane.index
|
|
1280
|
+
&& identity.pane_id === pane.pane_id
|
|
1281
|
+
&& identity.pid === pane.pid;
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
export async function reconcileScaleDownCleanupDebt(teamName, cwd, _callerConfig) {
|
|
1288
|
+
const sanitized = sanitizeTeamName(teamName);
|
|
1289
|
+
const leaderCwd = resolve(cwd);
|
|
1290
|
+
return await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
|
|
1291
|
+
await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
|
|
1292
|
+
// Authorization is derived only from the canonical state observed under the
|
|
1293
|
+
// membership barrier; callers may hold a stale pre-transaction config.
|
|
1294
|
+
const config = await readTeamConfig(sanitized, leaderCwd);
|
|
1295
|
+
if (!config)
|
|
1296
|
+
return { ok: false, error: `Team ${sanitized} not found` };
|
|
1297
|
+
const teamStateRoot = config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
|
|
1298
|
+
const cleanupDebtPath = join(teamStateRoot, 'team', sanitized, '.scale-down-cleanup-debt.json');
|
|
1299
|
+
if (!existsSync(cleanupDebtPath))
|
|
1300
|
+
return { ok: true };
|
|
1301
|
+
let debt;
|
|
1302
|
+
try {
|
|
1303
|
+
debt = JSON.parse(await readFile(cleanupDebtPath, 'utf8'));
|
|
1304
|
+
}
|
|
1305
|
+
catch (error) {
|
|
1306
|
+
return { ok: false, error: `scale_down_cleanup_debt_unreadable:${String(error)}` };
|
|
1307
|
+
}
|
|
1308
|
+
if (debt.schema_version !== 1 || debt.operation !== 'scale_down') {
|
|
1309
|
+
return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
|
|
1310
|
+
}
|
|
1311
|
+
const removedWorkerNames = Array.isArray(debt.removed_worker_names)
|
|
1312
|
+
? debt.removed_worker_names
|
|
1313
|
+
: (Array.isArray(debt.workers) ? debt.workers.map((worker) => worker?.name).filter((name) => typeof name === 'string') : []);
|
|
1314
|
+
if (removedWorkerNames.length === 0
|
|
1315
|
+
|| removedWorkerNames.some((name) => !/^worker-\d+$/.test(name))
|
|
1316
|
+
|| new Set(removedWorkerNames).size !== removedWorkerNames.length) {
|
|
1317
|
+
return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
|
|
1318
|
+
}
|
|
1319
|
+
const resources = asScaleDownDebtResources(debt, removedWorkerNames);
|
|
1320
|
+
if (!resources)
|
|
1321
|
+
return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
|
|
1322
|
+
try {
|
|
1323
|
+
await validateScaleDownCleanupResources(sanitized, leaderCwd, teamStateRoot, resources);
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
|
|
1327
|
+
}
|
|
1328
|
+
const panes = asScaleDownDebtPanes(debt, removedWorkerNames, resources, config.leader_pane_id, config.hud_pane_id);
|
|
1329
|
+
// Validate all journal bindings before classifying or mutating recovery state.
|
|
1330
|
+
// A journal is evidence, never authority to target an arbitrary global pane.
|
|
1331
|
+
if (!panes)
|
|
1332
|
+
return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
|
|
1333
|
+
const stillCanonical = removedWorkerNames.filter((name) => config.workers.some((worker) => worker.name === name));
|
|
1334
|
+
// A pre-commit journal is not authorization to kill or remove anything.
|
|
1335
|
+
if (stillCanonical.length === removedWorkerNames.length) {
|
|
1336
|
+
await removeDurableFile(cleanupDebtPath);
|
|
1337
|
+
return { ok: true };
|
|
1338
|
+
}
|
|
1339
|
+
if (stillCanonical.length > 0)
|
|
1340
|
+
return { ok: false, error: 'scale_down_cleanup_debt_membership_inconsistent' };
|
|
1341
|
+
// A cleanup journal may never reuse a pane identity held by a worker that
|
|
1342
|
+
// survived the committed membership update. This is checked before any
|
|
1343
|
+
// liveness or owner probes so malformed debt cannot cause a tmux effect.
|
|
1344
|
+
const survivingWorkers = config.workers.filter((worker) => !removedWorkerNames.includes(worker.name));
|
|
1345
|
+
if (panes.some((pane) => survivingWorkers.some((worker) => (worker.pane_id === pane.pane_id
|
|
1346
|
+
|| (pane.pid !== null && worker.pid === pane.pid))))) {
|
|
1347
|
+
return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
|
|
1348
|
+
}
|
|
1349
|
+
const resolvedPaneIds = new Set();
|
|
1350
|
+
const reasons = [];
|
|
1351
|
+
const expectedOwnerId = typeof config.tmux_pane_owner_id === 'string'
|
|
1352
|
+
? config.tmux_pane_owner_id.trim()
|
|
1353
|
+
: '';
|
|
1354
|
+
for (const pane of panes) {
|
|
1355
|
+
// Legacy PID-less records can establish convergence from an authoritative
|
|
1356
|
+
// global absence/dead proof, but can never authorize a live-pane effect.
|
|
1357
|
+
const freshProof = readExactPaneProofSync(pane.pane_id);
|
|
1358
|
+
if (freshProof.status === 'gone') {
|
|
1359
|
+
resolvedPaneIds.add(pane.pane_id);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (freshProof.status === 'unavailable') {
|
|
1363
|
+
reasons.push(`${pane.pane_id}:${freshProof.reason}`);
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
if (pane.pid === null) {
|
|
1367
|
+
reasons.push(`${pane.pane_id}:legacy_pid_missing_live`);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (freshProof.pid !== pane.pid) {
|
|
1371
|
+
reasons.push(`${pane.pane_id}:pane_pid_changed`);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
// A live debt pane must remain bound to the removed worker's recorded
|
|
1375
|
+
// identity; the team-wide owner token alone only proves team membership.
|
|
1376
|
+
if (!await hasMatchingScaleDownDebtWorkerIdentity(teamStateRoot, sanitized, pane)) {
|
|
1377
|
+
reasons.push(`${pane.pane_id}:worker_identity_unavailable`);
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
// Owner reads are authoritative only for this canonical Team token. Missing,
|
|
1381
|
+
// mismatched, and unavailable tags all fail closed for a live pane effect.
|
|
1382
|
+
if (!expectedOwnerId) {
|
|
1383
|
+
reasons.push(`${pane.pane_id}:team_owner_unavailable`);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
const owner = readPaneTeamOwnerTagResult(pane.pane_id);
|
|
1387
|
+
if (owner.status === 'error') {
|
|
1388
|
+
reasons.push(`${pane.pane_id}:team_owner_unavailable`);
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (owner.status !== 'value' || owner.value !== expectedOwnerId) {
|
|
1392
|
+
reasons.push(`${pane.pane_id}:team_owner_mismatch`);
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
// teardownWorkerPanes re-proves this exact pane immediately before the kill
|
|
1396
|
+
// and pins the same durable PID through the kill confirmation.
|
|
1397
|
+
const teardown = await teardownWorkerPanes([pane.pane_id], {
|
|
1398
|
+
leaderPaneId: config.leader_pane_id,
|
|
1399
|
+
hudPaneId: config.hud_pane_id,
|
|
1400
|
+
expectedPanePids: { [pane.pane_id]: pane.pid },
|
|
1401
|
+
authorizePaneKill: (paneId) => {
|
|
1402
|
+
const currentOwner = readPaneTeamOwnerTagResult(paneId);
|
|
1403
|
+
return currentOwner.status === 'value' && currentOwner.value === expectedOwnerId;
|
|
1404
|
+
},
|
|
1405
|
+
});
|
|
1406
|
+
if (teardown.provenGonePaneIds.includes(pane.pane_id) || teardown.killedPaneIds.includes(pane.pane_id)) {
|
|
1407
|
+
resolvedPaneIds.add(pane.pane_id);
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
reasons.push(...teardown.proofUnavailable.map((proof) => `${proof.paneId}:${proof.reason}`));
|
|
1411
|
+
reasons.push(...teardown.kill.failedPaneIds.map((paneId) => `${paneId}:kill_failed`));
|
|
1412
|
+
if (teardown.proofUnavailable.length === 0 && teardown.kill.failedPaneIds.length === 0) {
|
|
1413
|
+
reasons.push(`${pane.pane_id}:teardown_unresolved`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const unresolvedPanes = panes.filter((pane) => !resolvedPaneIds.has(pane.pane_id));
|
|
1417
|
+
if (unresolvedPanes.length > 0) {
|
|
1418
|
+
await writeAtomic(cleanupDebtPath, JSON.stringify({
|
|
1419
|
+
...debt,
|
|
1420
|
+
status: 'unresolved',
|
|
1421
|
+
updated_at: new Date().toISOString(),
|
|
1422
|
+
unresolved_panes: unresolvedPanes,
|
|
1423
|
+
reasons,
|
|
1424
|
+
}, null, 2));
|
|
1425
|
+
return { ok: false, error: `scale_down_cleanup_debt_unresolved:${unresolvedPanes.map((pane) => pane.pane_id).join(',')}` };
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
await cleanupScaleDownResources(sanitized, teamStateRoot, resources);
|
|
1429
|
+
}
|
|
1430
|
+
catch (error) {
|
|
1431
|
+
await writeAtomic(cleanupDebtPath, JSON.stringify({
|
|
1432
|
+
...debt,
|
|
1433
|
+
status: 'resource_cleanup_pending',
|
|
1434
|
+
updated_at: new Date().toISOString(),
|
|
1435
|
+
unresolved_panes: [],
|
|
1436
|
+
reasons: [`resource_cleanup_failed:${String(error)}`],
|
|
1437
|
+
}, null, 2));
|
|
1438
|
+
return { ok: false, error: `scale_down_cleanup_debt_resource_cleanup_failed:${String(error)}` };
|
|
1439
|
+
}
|
|
1440
|
+
await removeDurableFile(cleanupDebtPath);
|
|
1441
|
+
return { ok: true };
|
|
530
1442
|
});
|
|
531
1443
|
}
|
|
532
1444
|
/**
|
|
@@ -542,10 +1454,17 @@ export async function scaleDown(teamName, cwd, options = {}, env = process.env)
|
|
|
542
1454
|
const force = options.force === true;
|
|
543
1455
|
const drainTimeoutMs = options.drainTimeoutMs ?? 30_000;
|
|
544
1456
|
return await withScalingLock(sanitized, leaderCwd, async () => {
|
|
545
|
-
const config = await
|
|
1457
|
+
const config = await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
|
|
1458
|
+
await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
|
|
1459
|
+
return await readTeamConfig(sanitized, leaderCwd);
|
|
1460
|
+
});
|
|
546
1461
|
if (!config) {
|
|
547
1462
|
return { ok: false, error: `Team ${sanitized} not found` };
|
|
548
1463
|
}
|
|
1464
|
+
let teamStateRoot = config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
|
|
1465
|
+
const priorCleanup = await reconcileScaleDownCleanupDebt(sanitized, leaderCwd, config);
|
|
1466
|
+
if (!priorCleanup.ok)
|
|
1467
|
+
return priorCleanup;
|
|
549
1468
|
// Determine which workers to remove
|
|
550
1469
|
let targetWorkers;
|
|
551
1470
|
if (options.workerNames && options.workerNames.length > 0) {
|
|
@@ -595,6 +1514,33 @@ export async function scaleDown(teamName, cwd, options = {}, env = process.env)
|
|
|
595
1514
|
}
|
|
596
1515
|
const sessionName = config.tmux_session;
|
|
597
1516
|
const removedNames = [];
|
|
1517
|
+
const priorWorkerStatusArtifacts = new Map();
|
|
1518
|
+
for (const worker of targetWorkers) {
|
|
1519
|
+
const statusPath = join(teamStateRoot, 'team', sanitized, 'workers', worker.name, 'status.json');
|
|
1520
|
+
try {
|
|
1521
|
+
priorWorkerStatusArtifacts.set(worker.name, { exists: true, raw: await readFile(statusPath) });
|
|
1522
|
+
}
|
|
1523
|
+
catch (error) {
|
|
1524
|
+
if (error.code === 'ENOENT') {
|
|
1525
|
+
priorWorkerStatusArtifacts.set(worker.name, { exists: false });
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
throw error;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const restorePriorWorkerStatuses = async (workers) => {
|
|
1533
|
+
await Promise.all(workers.map(async (worker) => {
|
|
1534
|
+
const statusPath = join(teamStateRoot, 'team', sanitized, 'workers', worker.name, 'status.json');
|
|
1535
|
+
const priorArtifact = priorWorkerStatusArtifacts.get(worker.name);
|
|
1536
|
+
if (priorArtifact?.exists) {
|
|
1537
|
+
await writeFile(statusPath, priorArtifact.raw);
|
|
1538
|
+
}
|
|
1539
|
+
else {
|
|
1540
|
+
await rm(statusPath, { force: true });
|
|
1541
|
+
}
|
|
1542
|
+
}));
|
|
1543
|
+
};
|
|
598
1544
|
// Phase 1: Set workers to 'draining' status
|
|
599
1545
|
for (const w of targetWorkers) {
|
|
600
1546
|
const drainingStatus = {
|
|
@@ -611,69 +1557,239 @@ export async function scaleDown(teamName, cwd, options = {}, env = process.env)
|
|
|
611
1557
|
const allDrained = await Promise.all(targetWorkers.map(async (w) => {
|
|
612
1558
|
const status = await readWorkerStatus(sanitized, w.name, leaderCwd);
|
|
613
1559
|
return status.state === 'idle' || status.state === 'done' ||
|
|
614
|
-
status.state === 'draining' || !isWorkerAlive(sessionName, w.index, w.pane_id);
|
|
1560
|
+
status.state === 'draining' || !isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
615
1561
|
}));
|
|
616
1562
|
if (allDrained.every(Boolean))
|
|
617
1563
|
break;
|
|
618
1564
|
await new Promise(r => setTimeout(r, 2_000));
|
|
619
1565
|
}
|
|
620
1566
|
}
|
|
621
|
-
// Phase 3:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1567
|
+
// Phase 3: acquire the membership barrier and task claim locks before any
|
|
1568
|
+
// pane effect. The barrier and locks remain held through the canonical
|
|
1569
|
+
// snapshot, exact pane teardown, and forward-recoverable commit.
|
|
1570
|
+
let removableWorkers = targetWorkers;
|
|
1571
|
+
let removableWorkerNames = new Set(removableWorkers.map((worker) => worker.name));
|
|
1572
|
+
let teardownFailure = null;
|
|
1573
|
+
try {
|
|
1574
|
+
await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
|
|
1575
|
+
await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
|
|
1576
|
+
const authoritativeConfig = await readTeamConfig(sanitized, leaderCwd);
|
|
1577
|
+
if (!authoritativeConfig)
|
|
1578
|
+
throw new Error('canonical_scale_down_config_missing');
|
|
1579
|
+
const authoritativeWorkers = removableWorkers.map((worker) => authoritativeConfig.workers.find((candidate) => candidate.name === worker.name));
|
|
1580
|
+
if (authoritativeWorkers.some((worker) => !worker)) {
|
|
1581
|
+
throw new Error('canonical_scale_down_membership_changed');
|
|
1582
|
+
}
|
|
1583
|
+
// Capture targets from the canonical generation while the membership
|
|
1584
|
+
// authority is held; no caller-owned worker record authorizes a pane effect.
|
|
1585
|
+
removableWorkers = authoritativeWorkers;
|
|
1586
|
+
removableWorkerNames = new Set(removableWorkers.map((worker) => worker.name));
|
|
1587
|
+
teamStateRoot = authoritativeConfig.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
|
|
1588
|
+
Object.assign(config, authoritativeConfig);
|
|
1589
|
+
const candidateTaskIds = (await listTasks(sanitized, leaderCwd))
|
|
1590
|
+
.filter((task) => task.status !== 'completed' && task.status !== 'failed')
|
|
1591
|
+
.map((task) => task.id);
|
|
1592
|
+
await withTaskClaimLocks(sanitized, candidateTaskIds, leaderCwd, async () => {
|
|
1593
|
+
const lockedTasks = await listTasks(sanitized, leaderCwd);
|
|
1594
|
+
const configPath = join(teamStateRoot, 'team', sanitized, 'config.json');
|
|
1595
|
+
const configSnapshot = await readFile(configPath);
|
|
1596
|
+
const manifestPath = join(teamStateRoot, 'team', sanitized, 'manifest.v2.json');
|
|
1597
|
+
const manifestSnapshot = existsSync(manifestPath) ? await readFile(manifestPath) : null;
|
|
1598
|
+
const reconciledTasks = lockedTasks.filter((task) => task.status !== 'completed' && task.status !== 'failed'
|
|
1599
|
+
&& (removableWorkerNames.has(task.owner ?? '') || removableWorkerNames.has(task.claim?.owner ?? '')));
|
|
1600
|
+
const taskSnapshots = new Map();
|
|
1601
|
+
for (const task of reconciledTasks) {
|
|
1602
|
+
taskSnapshots.set(task.id, await readFile(join(teamStateRoot, 'team', sanitized, 'tasks', `task-${task.id}.json`)));
|
|
1603
|
+
}
|
|
1604
|
+
const postSnapshotMarker = env.OMX_TEAM_SCALE_DOWN_POST_SNAPSHOT_MARKER;
|
|
1605
|
+
if (postSnapshotMarker)
|
|
1606
|
+
await writeFile(postSnapshotMarker, 'snapshot-held\n');
|
|
1607
|
+
const boundaryHoldMs = Number.parseInt(env.OMX_TEAM_SCALE_DOWN_BOUNDARY_HOLD_MS ?? '', 10);
|
|
1608
|
+
if (Number.isFinite(boundaryHoldMs) && boundaryHoldMs > 0)
|
|
1609
|
+
await new Promise((resolve) => setTimeout(resolve, boundaryHoldMs));
|
|
1610
|
+
const currentConfig = JSON.parse(configSnapshot.toString('utf8'));
|
|
1611
|
+
const nextConfig = {
|
|
1612
|
+
...currentConfig,
|
|
1613
|
+
workers: currentConfig.workers.filter((worker) => !removableWorkerNames.has(worker.name)),
|
|
1614
|
+
};
|
|
1615
|
+
nextConfig.worker_count = nextConfig.workers.length;
|
|
1616
|
+
const nextManifest = manifestSnapshot
|
|
1617
|
+
? JSON.stringify({
|
|
1618
|
+
...JSON.parse(manifestSnapshot.toString('utf8')),
|
|
1619
|
+
workers: nextConfig.workers,
|
|
1620
|
+
worker_count: nextConfig.worker_count,
|
|
1621
|
+
}, null, 2)
|
|
1622
|
+
: null;
|
|
1623
|
+
const membershipTransaction = {
|
|
1624
|
+
baseGeneration: currentConfig.config_generation ?? 0,
|
|
1625
|
+
tasks: reconciledTasks.map((task) => ({
|
|
1626
|
+
taskId: task.id,
|
|
1627
|
+
oldBytes: taskSnapshots.get(task.id)?.toString('utf8') ?? null,
|
|
1628
|
+
newBytes: JSON.stringify({
|
|
1629
|
+
...task,
|
|
1630
|
+
owner: undefined,
|
|
1631
|
+
claim: undefined,
|
|
1632
|
+
status: task.status === 'in_progress' ? 'pending' : task.status,
|
|
1633
|
+
version: Math.max(1, task.version ?? 1) + 1,
|
|
1634
|
+
}, null, 2),
|
|
1635
|
+
})),
|
|
1636
|
+
config: {
|
|
1637
|
+
oldBytes: configSnapshot.toString('utf8'),
|
|
1638
|
+
newBytes: JSON.stringify(nextConfig, null, 2),
|
|
1639
|
+
},
|
|
1640
|
+
manifest: {
|
|
1641
|
+
oldBytes: manifestSnapshot?.toString('utf8') ?? null,
|
|
1642
|
+
newBytes: nextManifest,
|
|
1643
|
+
},
|
|
1644
|
+
interruptAfterFirstTaskWrite: env.OMX_TEAM_SCALE_DOWN_INJECT_FAILURE === 'after-first-task-write',
|
|
1645
|
+
failRollbackPersistence: env.OMX_TEAM_SCALE_DOWN_INJECT_FAILURE === 'rollback-persistence-failure',
|
|
1646
|
+
recoverToNewOnFailure: true,
|
|
1647
|
+
};
|
|
1648
|
+
const cleanupDebtPath = join(teamStateRoot, 'team', sanitized, '.scale-down-cleanup-debt.json');
|
|
1649
|
+
const cleanupDebtBase = {
|
|
1650
|
+
schema_version: 1,
|
|
1651
|
+
operation: 'scale_down',
|
|
1652
|
+
status: 'pending_teardown',
|
|
1653
|
+
created_at: new Date().toISOString(),
|
|
1654
|
+
removed_worker_names: removableWorkers.map((worker) => worker.name),
|
|
1655
|
+
workers: removableWorkers
|
|
1656
|
+
.filter((worker) => (typeof worker.pane_id === 'string' && /^%\d+$/.test(worker.pane_id)))
|
|
1657
|
+
.map((worker) => ({
|
|
1658
|
+
name: worker.name,
|
|
1659
|
+
index: worker.index,
|
|
1660
|
+
pane_id: worker.pane_id,
|
|
1661
|
+
pid: worker.pid ?? null,
|
|
1662
|
+
})),
|
|
1663
|
+
resource_workers: removableWorkers.map((worker) => ({
|
|
1664
|
+
name: worker.name,
|
|
1665
|
+
worktree_path: worker.worktree_path,
|
|
1666
|
+
worktree_repo_root: worker.worktree_repo_root,
|
|
1667
|
+
worktree_branch: worker.worktree_branch,
|
|
1668
|
+
worktree_detached: worker.worktree_detached,
|
|
1669
|
+
worktree_created: worker.worktree_created,
|
|
1670
|
+
team_state_root: worker.team_state_root,
|
|
1671
|
+
})),
|
|
1672
|
+
};
|
|
1673
|
+
await writeAtomic(cleanupDebtPath, JSON.stringify(cleanupDebtBase, null, 2));
|
|
1674
|
+
await commitTeamMembershipTaskTransaction(sanitized, leaderCwd, membershipTransaction);
|
|
1675
|
+
const committed = await readTeamConfig(sanitized, leaderCwd);
|
|
1676
|
+
if (!committed || committed.workers.some((worker) => removableWorkerNames.has(worker.name))) {
|
|
1677
|
+
throw new Error('canonical_scale_down_config_verification_failed');
|
|
1678
|
+
}
|
|
1679
|
+
config.workers = committed.workers;
|
|
1680
|
+
config.worker_count = committed.worker_count;
|
|
1681
|
+
for (const task of reconciledTasks) {
|
|
1682
|
+
const reconciled = await readTask(sanitized, task.id, leaderCwd);
|
|
1683
|
+
if (reconciled && (removableWorkerNames.has(reconciled.owner ?? '') || removableWorkerNames.has(reconciled.claim?.owner ?? ''))) {
|
|
1684
|
+
throw new Error(`canonical_scale_down_task_verification_failed:${task.id}`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
// PID-less rollback records are cleanup debt, not effect authority. A
|
|
1688
|
+
// fresh gone proof may resolve one, but a live or unavailable proof
|
|
1689
|
+
// must remain debt rather than letting teardown adopt its current PID.
|
|
1690
|
+
const pinnedPaneWorkers = removableWorkers.filter((worker) => (typeof worker.pane_id === 'string'
|
|
1691
|
+
&& /^%\d+$/.test(worker.pane_id)
|
|
1692
|
+
&& typeof worker.pid === 'number'
|
|
1693
|
+
&& Number.isSafeInteger(worker.pid)
|
|
1694
|
+
&& worker.pid > 0));
|
|
1695
|
+
const unpinnedPaneWorkers = removableWorkers.filter((worker) => (typeof worker.pane_id === 'string'
|
|
1696
|
+
&& /^%\d+$/.test(worker.pane_id)
|
|
1697
|
+
&& !pinnedPaneWorkers.some((pinnedWorker) => pinnedWorker.name === worker.name)));
|
|
1698
|
+
const unpinnedResolvedPaneIds = new Set();
|
|
1699
|
+
const unpinnedProofDebt = [];
|
|
1700
|
+
for (const worker of unpinnedPaneWorkers) {
|
|
1701
|
+
const proof = readExactPaneProofSync(worker.pane_id);
|
|
1702
|
+
if (proof.status === 'gone') {
|
|
1703
|
+
unpinnedResolvedPaneIds.add(worker.pane_id);
|
|
1704
|
+
}
|
|
1705
|
+
else if (proof.status === 'unavailable') {
|
|
1706
|
+
unpinnedProofDebt.push(`${worker.pane_id}:${proof.reason}`);
|
|
1707
|
+
}
|
|
1708
|
+
else {
|
|
1709
|
+
unpinnedProofDebt.push(`${worker.pane_id}:legacy_pid_missing_live`);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
const targetPaneIds = pinnedPaneWorkers.map((worker) => worker.pane_id);
|
|
1713
|
+
const expectedScaleDownOwnerId = typeof config.tmux_pane_owner_id === 'string'
|
|
1714
|
+
? config.tmux_pane_owner_id.trim()
|
|
1715
|
+
: '';
|
|
1716
|
+
const paneTeardown = await teardownWorkerPanes(targetPaneIds, {
|
|
1717
|
+
leaderPaneId: config.leader_pane_id,
|
|
1718
|
+
hudPaneId: config.hud_pane_id,
|
|
1719
|
+
expectedPanePids: Object.fromEntries(pinnedPaneWorkers.map((worker) => [worker.pane_id, worker.pid])),
|
|
1720
|
+
authorizePaneKill: (paneId) => {
|
|
1721
|
+
const currentOwner = readPaneTeamOwnerTagResult(paneId);
|
|
1722
|
+
return Boolean(expectedScaleDownOwnerId)
|
|
1723
|
+
&& currentOwner.status === 'value'
|
|
1724
|
+
&& currentOwner.value === expectedScaleDownOwnerId;
|
|
1725
|
+
},
|
|
1726
|
+
});
|
|
1727
|
+
const resolvedPaneIds = new Set([
|
|
1728
|
+
...unpinnedResolvedPaneIds,
|
|
1729
|
+
...paneTeardown.provenGonePaneIds,
|
|
1730
|
+
...paneTeardown.killedPaneIds,
|
|
1731
|
+
]);
|
|
1732
|
+
const unresolvedWorkers = removableWorkers.filter((worker) => (typeof worker.pane_id === 'string' && !resolvedPaneIds.has(worker.pane_id)));
|
|
1733
|
+
if (unresolvedWorkers.length > 0) {
|
|
1734
|
+
await restorePriorWorkerStatuses(unresolvedWorkers);
|
|
1735
|
+
const proofDebt = [...unpinnedProofDebt, ...paneTeardown.proofUnavailable.map((proof) => `${proof.paneId}:${proof.reason}`)];
|
|
1736
|
+
const killDebt = paneTeardown.kill.failedPaneIds.map((paneId) => `${paneId}:kill_failed`);
|
|
1737
|
+
await writeAtomic(cleanupDebtPath, JSON.stringify({
|
|
1738
|
+
...cleanupDebtBase,
|
|
1739
|
+
status: 'unresolved',
|
|
1740
|
+
updated_at: new Date().toISOString(),
|
|
1741
|
+
unresolved_panes: unresolvedWorkers.map((worker) => ({
|
|
1742
|
+
name: worker.name,
|
|
1743
|
+
index: worker.index,
|
|
1744
|
+
pane_id: worker.pane_id,
|
|
1745
|
+
pid: worker.pid ?? null,
|
|
1746
|
+
})),
|
|
1747
|
+
reasons: [...proofDebt, ...killDebt],
|
|
1748
|
+
}, null, 2));
|
|
1749
|
+
if (paneTeardown.proofUnavailable.length > 0) {
|
|
1750
|
+
teardownFailure = { ok: false, error: `scale_down_pane_proof_unavailable:${proofDebt.join(',')}` };
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const unresolvedDebt = unresolvedWorkers
|
|
1754
|
+
.map((worker) => `pane_teardown_unresolved:${worker.pane_id}`)
|
|
1755
|
+
.join(';');
|
|
1756
|
+
const failedDebt = paneTeardown.kill.failedPaneIds.length > 0
|
|
1757
|
+
? `pane_teardown_failed:${paneTeardown.kill.failedPaneIds.join(',')};`
|
|
1758
|
+
: '';
|
|
1759
|
+
teardownFailure = { ok: false, error: `scale_down_cleanup_debt:${failedDebt}${unresolvedDebt}` };
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
await writeAtomic(cleanupDebtPath, JSON.stringify({
|
|
1763
|
+
...cleanupDebtBase,
|
|
1764
|
+
status: 'resource_cleanup_pending',
|
|
1765
|
+
updated_at: new Date().toISOString(),
|
|
1766
|
+
unresolved_panes: [],
|
|
1767
|
+
}, null, 2));
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
655
1770
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
await
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
1771
|
+
catch (error) {
|
|
1772
|
+
await restorePriorWorkerStatuses(removableWorkers);
|
|
1773
|
+
return { ok: false, error: `scale_down_task_reconciliation_failed:${String(error)}` };
|
|
1774
|
+
}
|
|
1775
|
+
if (teardownFailure)
|
|
1776
|
+
return teardownFailure;
|
|
1777
|
+
targetWorkers = removableWorkers;
|
|
1778
|
+
// Resource cleanup remains represented by the committed debt until every
|
|
1779
|
+
// worker directory, status, generated AGENTS file, and provisioned worktree
|
|
1780
|
+
// has converged. Reconciliation reacquires authority before consuming it.
|
|
1781
|
+
removedNames.push(...targetWorkers.map((worker) => worker.name));
|
|
1782
|
+
const resourceCleanup = await reconcileScaleDownCleanupDebt(sanitized, leaderCwd, config);
|
|
1783
|
+
if (!resourceCleanup.ok)
|
|
1784
|
+
return resourceCleanup;
|
|
1785
|
+
const reason = `scale_down: removed ${removedNames.length} worker(s) [${removedNames.join(', ')}], new count=${config.worker_count}`;
|
|
1786
|
+
try {
|
|
1787
|
+
await appendTeamEvent(sanitized, { type: 'team_leader_nudge', worker: 'leader-fixed', reason }, leaderCwd);
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
// The cleanup transaction has already converged; event delivery is advisory.
|
|
1791
|
+
}
|
|
1792
|
+
return { ok: true, removedWorkers: removedNames, newWorkerCount: config.worker_count };
|
|
677
1793
|
});
|
|
678
1794
|
}
|
|
679
1795
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|