oh-my-codex 0.20.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +35 -18
- package/Cargo.toml +1 -1
- package/README.md +22 -19
- package/crates/omx-runtime/Cargo.toml +3 -0
- package/crates/omx-runtime/src/main.rs +390 -0
- package/crates/omx-runtime/tests/execution.rs +264 -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 +0 -27
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +3 -8
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +0 -41
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/policy.d.ts.map +1 -1
- package/dist/agents/policy.js +0 -1
- package/dist/agents/policy.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 +16 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +22 -7
- 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__/completion-gate-advisory.test.d.ts +2 -0
- package/dist/autopilot/__tests__/completion-gate-advisory.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/completion-gate-advisory.test.js +383 -0
- package/dist/autopilot/__tests__/completion-gate-advisory.test.js.map +1 -0
- package/dist/autopilot/completion-gate.d.ts +6 -1
- package/dist/autopilot/completion-gate.d.ts.map +1 -1
- package/dist/autopilot/completion-gate.js +247 -7
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/catalog/__tests__/generator.test.js +15 -10
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +13 -7
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
- package/dist/catalog/__tests__/schema.test.js +3 -5
- package/dist/catalog/__tests__/schema.test.js.map +1 -1
- package/dist/catalog/schema.js +1 -1
- package/dist/catalog/schema.js.map +1 -1
- package/dist/cli/__tests__/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 +3 -1
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autopilot.test.d.ts +2 -0
- package/dist/cli/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/cli/__tests__/autopilot.test.js +143 -0
- package/dist/cli/__tests__/autopilot.test.js.map +1 -0
- 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 +76 -4
- 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 +206 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-team.test.js +85 -0
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +38 -6
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +75 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +621 -460
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1125 -143
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +4 -3
- 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 +53 -5
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +5 -8
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js +4 -18
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +4 -30
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +4 -15
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +2 -2
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
- 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 +131 -14
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1153 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +65 -3
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +57 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +34 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +258 -26
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +3 -6
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +180 -21
- package/dist/cli/__tests__/setup-skills-overwrite.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 +45 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +161 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +73 -28
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +749 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -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/autopilot.d.ts +8 -0
- package/dist/cli/autopilot.d.ts.map +1 -0
- package/dist/cli/autopilot.js +160 -0
- package/dist/cli/autopilot.js.map +1 -0
- 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 +29 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +433 -19
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +231 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2354 -724
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.d.ts.map +1 -1
- package/dist/cli/mcp-parity.js +7 -3
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +26 -1
- 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 +8 -4
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +30 -8
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +69 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +338 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/plugin-marketplace.d.ts +1 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +123 -55
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +6 -2
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +134 -34
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +38 -1
- 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 +6 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +425 -215
- 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 +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +10 -7
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +27 -28
- 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 +16 -23
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +212 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts +2 -0
- package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts.map +1 -0
- package/dist/compat/__tests__/upgrade-from-0-20.test.js +109 -0
- package/dist/compat/__tests__/upgrade-from-0-20.test.js.map +1 -0
- package/dist/compat/upgrade-from-0-20.d.ts +64 -0
- package/dist/compat/upgrade-from-0-20.d.ts.map +1 -0
- package/dist/compat/upgrade-from-0-20.js +276 -0
- package/dist/compat/upgrade-from-0-20.js.map +1 -0
- package/dist/config/__tests__/generator-idempotent.test.js +173 -29
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +3 -1
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +25 -3
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +113 -35
- package/dist/config/generator.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 +3 -1
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js +24 -31
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +66 -135
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +47 -106
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +16 -18
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +2 -14
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js +56 -234
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +1 -3
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js +18 -16
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +24 -20
- package/dist/hooks/__tests__/explore-sparkshell-guidance-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 +164 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js +169 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +226 -858
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +176 -14
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +33 -21
- 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-ralph-resume.test.js +14 -904
- package/dist/hooks/__tests__/notify-hook-ralph-resume.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-session-scope.test.js +5 -5
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +7 -7
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +11 -8
- 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 +7 -6
- 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-worker-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js +19 -21
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +23 -3
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +23 -28
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/__tests__/research-workflow-boundaries.test.js +9 -8
- package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3656 -656
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +13 -22
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-guidance-contract.test.js +3 -13
- package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/sunset-routing-contract.test.js +172 -0
- package/dist/hooks/__tests__/sunset-routing-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +2 -2
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +18 -3
- package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js +20 -17
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js +17 -18
- package/dist/hooks/__tests__/visual-verdict-loop.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 +6 -47
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +156 -268
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +14 -26
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/native/capability-warnings.d.ts +16 -0
- package/dist/hooks/native/capability-warnings.d.ts.map +1 -0
- package/dist/hooks/native/capability-warnings.js +40 -0
- package/dist/hooks/native/capability-warnings.js.map +1 -0
- package/dist/hooks/native/index.d.ts +17 -0
- package/dist/hooks/native/index.d.ts.map +1 -0
- package/dist/hooks/native/index.js +17 -0
- package/dist/hooks/native/index.js.map +1 -0
- package/dist/hooks/native/pre-tool-use-advisory.d.ts +17 -0
- package/dist/hooks/native/pre-tool-use-advisory.d.ts.map +1 -0
- package/dist/hooks/native/pre-tool-use-advisory.js +112 -0
- package/dist/hooks/native/pre-tool-use-advisory.js.map +1 -0
- package/dist/hooks/native/types.d.ts +30 -0
- package/dist/hooks/native/types.d.ts.map +1 -0
- package/dist/hooks/native/types.js +22 -0
- package/dist/hooks/native/types.js.map +1 -0
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +46 -93
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/session.d.ts +197 -18
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1939 -271
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/sunset-stub.d.ts +17 -0
- package/dist/hooks/sunset-stub.d.ts.map +1 -0
- package/dist/hooks/sunset-stub.js +125 -0
- package/dist/hooks/sunset-stub.js.map +1 -0
- 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-split-realtmux.test.d.ts +2 -0
- package/dist/hud/__tests__/tmux-split-realtmux.test.d.ts.map +1 -0
- package/dist/hud/__tests__/tmux-split-realtmux.test.js +112 -0
- package/dist/hud/__tests__/tmux-split-realtmux.test.js.map +1 -0
- package/dist/hud/__tests__/tmux.test.js +217 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +43 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1025 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +71 -34
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +1 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +25 -24
- package/dist/leader/contract.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-ralph-phase.test.js +29 -4
- package/dist/mcp/__tests__/state-server-ralph-phase.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-schema.test.js +13 -3
- package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +11 -1033
- 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/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 +68 -97
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +116 -67
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +1 -168
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js +1 -85
- package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -35
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +10 -158
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts +6 -6
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +116 -90
- 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/__tests__/formatter.test.js +8 -0
- package/dist/notifications/__tests__/formatter.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js +8 -0
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/formatter.js +1 -1
- package/dist/notifications/formatter.js.map +1 -1
- package/dist/notifications/template-engine.js +1 -1
- package/dist/notifications/template-engine.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js +0 -21
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/question/autopilot-wait.d.ts.map +1 -1
- package/dist/question/autopilot-wait.js +2 -2
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/deep-interview.d.ts +1 -2
- package/dist/question/deep-interview.d.ts.map +1 -1
- package/dist/question/deep-interview.js +4 -4
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +56 -821
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
- package/dist/ralplan/documented-leader-preflight.js +356 -54
- package/dist/ralplan/documented-leader-preflight.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +2 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +145 -163
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +45 -0
- 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 +12490 -19414
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/docs-site-contract.test.js +2 -2
- package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -1
- package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts +2 -0
- package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/generated-artifact-drift.test.js +84 -0
- package/dist/scripts/__tests__/generated-artifact-drift.test.js.map +1 -0
- 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 +235 -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 +207 -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 +156 -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 +191 -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 +40 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js +63 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js +65 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.js +189 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.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__/prompt-inventory.test.js +54 -1
- package/dist/scripts/__tests__/prompt-inventory.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +2 -2
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -60
- 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 +60 -25
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +168 -20
- 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 +51 -2
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +3648 -1332
- 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 +33 -22
- 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 +28 -39
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -48
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +1 -0
- 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.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +8 -5
- 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 +32 -3
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +3 -2
- 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 +23 -3
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +45 -5
- 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 +4 -1
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +71 -124
- 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/prompt-inventory.d.ts +39 -0
- package/dist/scripts/prompt-inventory.d.ts.map +1 -1
- package/dist/scripts/prompt-inventory.js +108 -3
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/run-compiled-ci.js +2 -0
- package/dist/scripts/run-compiled-ci.js.map +1 -1
- package/dist/scripts/run-test-files.js +1 -0
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +32 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +407 -60
- 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/sync-prompt-guidance-fragments.js +62 -20
- package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
- package/dist/scripts/tmux-hook-engine.d.ts +1 -1
- package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
- package/dist/scripts/tmux-hook-engine.js +3 -3
- package/dist/scripts/tmux-hook-engine.js.map +1 -1
- package/dist/scripts/verify-capabilities-lock.d.ts +3 -0
- package/dist/scripts/verify-capabilities-lock.d.ts.map +1 -0
- package/dist/scripts/verify-capabilities-lock.js +22 -0
- package/dist/scripts/verify-capabilities-lock.js.map +1 -0
- 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__/authority-decreasing.test.d.ts +2 -0
- package/dist/state/__tests__/authority-decreasing.test.d.ts.map +1 -0
- package/dist/state/__tests__/authority-decreasing.test.js +78 -0
- package/dist/state/__tests__/authority-decreasing.test.js.map +1 -0
- package/dist/state/__tests__/handoff-carrier.test.d.ts +2 -0
- package/dist/state/__tests__/handoff-carrier.test.d.ts.map +1 -0
- package/dist/state/__tests__/handoff-carrier.test.js +96 -0
- package/dist/state/__tests__/handoff-carrier.test.js.map +1 -0
- package/dist/state/__tests__/mode-state-context.test.js +12 -5
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +870 -2517
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/single-writer-invariant.test.d.ts +2 -0
- package/dist/state/__tests__/single-writer-invariant.test.d.ts.map +1 -0
- package/dist/state/__tests__/single-writer-invariant.test.js +342 -0
- package/dist/state/__tests__/single-writer-invariant.test.js.map +1 -0
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/stale-state-neutralization.test.d.ts +2 -0
- package/dist/state/__tests__/stale-state-neutralization.test.d.ts.map +1 -0
- package/dist/state/__tests__/stale-state-neutralization.test.js +84 -0
- package/dist/state/__tests__/stale-state-neutralization.test.js.map +1 -0
- package/dist/state/__tests__/workflow-transition.test.js +20 -33
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/handoff-carrier.d.ts +46 -0
- package/dist/state/handoff-carrier.d.ts.map +1 -0
- package/dist/state/handoff-carrier.js +85 -0
- package/dist/state/handoff-carrier.js.map +1 -0
- package/dist/state/mode-state-context.d.ts +3 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +27 -13
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/namespace-owners.d.ts +34 -0
- package/dist/state/namespace-owners.d.ts.map +1 -0
- package/dist/state/namespace-owners.js +127 -0
- package/dist/state/namespace-owners.js.map +1 -0
- package/dist/state/operations.d.ts +9 -4
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +194 -235
- 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 +421 -31
- 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 +11 -31
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -27
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +9 -790
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
- 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 +64 -60
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +809 -358
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +1 -0
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +104 -3
- package/dist/team/__tests__/mcp-comm.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.test.js +725 -59
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +269 -245
- 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__/team-ops-contract.test.js +1 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +1093 -101
- 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 +159 -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-bootstrap.test.js +11 -13
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/__tests__/worker-provenance.test.d.ts +2 -0
- package/dist/team/__tests__/worker-provenance.test.d.ts.map +1 -0
- package/dist/team/__tests__/worker-provenance.test.js +251 -0
- package/dist/team/__tests__/worker-provenance.test.js.map +1 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +14 -8
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/mcp-comm.d.ts.map +1 -1
- package/dist/team/mcp-comm.js +3 -8
- package/dist/team/mcp-comm.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 +3 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +314 -142
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +161 -91
- package/dist/team/scaling.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/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 +19 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +47 -1
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +1 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +1 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +42 -4
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +572 -196
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/ultragoal-context.d.ts +1 -0
- package/dist/team/ultragoal-context.d.ts.map +1 -1
- package/dist/team/ultragoal-context.js +2 -0
- package/dist/team/ultragoal-context.js.map +1 -1
- package/dist/team/worker-provenance.d.ts +35 -0
- package/dist/team/worker-provenance.d.ts.map +1 -0
- package/dist/team/worker-provenance.js +213 -0
- package/dist/team/worker-provenance.js.map +1 -0
- package/dist/ultragoal/__tests__/artifacts.test.js +1053 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +89 -59
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +36 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +267 -51
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/__tests__/url-reader.test.js +14 -0
- package/dist/url-reader/__tests__/url-reader.test.js.map +1 -1
- package/dist/url-reader/index.js +5 -3
- package/dist/url-reader/index.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.js +58 -1
- package/dist/utils/__tests__/file-durability.test.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/__tests__/version.test.js +17 -0
- package/dist/utils/__tests__/version.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +10 -0
- package/dist/utils/file-durability.d.ts.map +1 -1
- package/dist/utils/file-durability.js +34 -4
- package/dist/utils/file-durability.js.map +1 -1
- 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/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +2 -1
- package/dist/utils/version.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__/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/package.json +17 -6
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +60 -135
- package/plugins/oh-my-codex/skills/analyze/SKILL.md +35 -111
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +72 -163
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +33 -392
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +66 -256
- package/plugins/oh-my-codex/skills/configure-notifications/SKILL.md +30 -216
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +11 -10
- package/plugins/oh-my-codex/skills/design/SKILL.md +32 -132
- package/plugins/oh-my-codex/skills/doctor/SKILL.md +56 -177
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +35 -116
- package/plugins/oh-my-codex/skills/plan/SKILL.md +45 -228
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +20 -22
- package/plugins/oh-my-codex/skills/skill/SKILL.md +82 -807
- package/plugins/oh-my-codex/skills/team/SKILL.md +32 -520
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +47 -150
- package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +75 -252
- package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +33 -111
- package/plugins/oh-my-codex/skills/worker/SKILL.md +40 -113
- package/prompts/analyst.md +27 -107
- package/prompts/api-reviewer.md +17 -84
- package/prompts/architect.md +31 -70
- package/prompts/code-reviewer.md +18 -107
- package/prompts/code-simplifier.md +32 -110
- package/prompts/critic.md +32 -54
- package/prompts/dependency-expert.md +37 -107
- package/prompts/designer.md +40 -107
- package/prompts/executor.md +26 -75
- package/prompts/information-architect.md +51 -200
- package/prompts/performance-reviewer.md +17 -81
- package/prompts/planner.md +33 -76
- package/prompts/product-analyst.md +65 -280
- package/prompts/product-manager.md +48 -216
- package/prompts/qa-tester.md +33 -104
- package/prompts/quality-reviewer.md +17 -93
- package/prompts/quality-strategist.md +31 -255
- package/prompts/researcher.md +32 -89
- package/prompts/security-reviewer.md +12 -98
- package/prompts/test-engineer.md +29 -110
- package/prompts/ux-researcher.md +46 -300
- package/prompts/verifier.md +23 -51
- package/skills/ai-slop-cleaner/SKILL.md +60 -135
- package/skills/analyze/SKILL.md +35 -111
- package/skills/autopilot/SKILL.md +72 -163
- package/skills/autoresearch-goal/SKILL.md +6 -31
- package/skills/cancel/SKILL.md +33 -392
- package/skills/code-review/SKILL.md +66 -256
- package/skills/configure-notifications/SKILL.md +30 -216
- package/skills/deep-interview/SKILL.md +11 -10
- package/skills/design/SKILL.md +32 -132
- package/skills/doctor/SKILL.md +56 -177
- package/skills/omx-setup/SKILL.md +35 -116
- package/skills/pipeline/SKILL.md +6 -89
- package/skills/plan/SKILL.md +45 -228
- package/skills/ralph/SKILL.md +6 -290
- package/skills/ralplan/SKILL.md +20 -22
- package/skills/skill/SKILL.md +82 -807
- package/skills/team/SKILL.md +32 -520
- package/skills/ultragoal/SKILL.md +47 -150
- package/skills/ultraqa/SKILL.md +75 -252
- package/skills/ultrawork/SKILL.md +6 -182
- package/skills/visual-ralph/SKILL.md +33 -111
- package/skills/worker/SKILL.md +40 -113
- package/src/scripts/__tests__/codex-native-hook.test.ts +18888 -28799
- package/src/scripts/__tests__/docs-site-contract.test.ts +2 -2
- package/src/scripts/__tests__/generated-artifact-drift.test.ts +97 -0
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +197 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +186 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +177 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +201 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +43 -0
- package/src/scripts/__tests__/issue-3481-conductor-deadlock.test.ts +77 -0
- package/src/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.ts +95 -0
- package/src/scripts/__tests__/issue-3536-external-team-state-root.test.ts +216 -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__/prompt-inventory.test.ts +62 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +1 -2
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +49 -59
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
- package/src/scripts/__tests__/smoke-packed-install.test.ts +223 -23
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +3618 -1383
- package/src/scripts/codex-native-pre-post.ts +50 -24
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +38 -38
- package/src/scripts/notify-hook/auto-nudge.ts +1 -0
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +8 -5
- package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
- package/src/scripts/notify-hook/team-tmux-guard.ts +3 -1
- package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
- package/src/scripts/notify-hook/team-worker.ts +43 -5
- package/src/scripts/notify-hook/tmux-injection.ts +5 -1
- package/src/scripts/notify-hook.ts +76 -124
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/prompt-inventory.ts +142 -3
- package/src/scripts/run-compiled-ci.ts +2 -0
- package/src/scripts/run-test-files.ts +1 -0
- package/src/scripts/smoke-packed-install.ts +440 -59
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/sync-prompt-guidance-fragments.ts +59 -22
- package/src/scripts/tmux-hook-engine.ts +3 -3
- package/src/scripts/verify-capabilities-lock.ts +23 -0
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +45 -9
- package/templates/catalog-manifest.json +6 -146
- package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts +0 -2
- package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts.map +0 -1
- package/dist/autopilot/__tests__/deep-interview-gate.test.js +0 -215
- package/dist/autopilot/__tests__/deep-interview-gate.test.js.map +0 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +0 -2
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +0 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +0 -1361
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +0 -1
- package/dist/autopilot/deep-interview-gate.d.ts +0 -18
- package/dist/autopilot/deep-interview-gate.d.ts.map +0 -1
- package/dist/autopilot/deep-interview-gate.js +0 -396
- package/dist/autopilot/deep-interview-gate.js.map +0 -1
- package/dist/autopilot/ralplan-gate.d.ts +0 -18
- package/dist/autopilot/ralplan-gate.d.ts.map +0 -1
- package/dist/autopilot/ralplan-gate.js +0 -148
- package/dist/autopilot/ralplan-gate.js.map +0 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js +0 -320
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +0 -1
- package/dist/pipeline/__tests__/orchestrator.test.d.ts +0 -2
- package/dist/pipeline/__tests__/orchestrator.test.d.ts.map +0 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +0 -770
- package/dist/pipeline/__tests__/orchestrator.test.js.map +0 -1
- package/dist/pipeline/__tests__/stages.test.d.ts +0 -2
- package/dist/pipeline/__tests__/stages.test.d.ts.map +0 -1
- package/dist/pipeline/__tests__/stages.test.js +0 -1778
- package/dist/pipeline/__tests__/stages.test.js.map +0 -1
- package/dist/pipeline/index.d.ts +0 -25
- package/dist/pipeline/index.d.ts.map +0 -1
- package/dist/pipeline/index.js +0 -17
- package/dist/pipeline/index.js.map +0 -1
- package/dist/pipeline/orchestrator.d.ts +0 -52
- package/dist/pipeline/orchestrator.d.ts.map +0 -1
- package/dist/pipeline/orchestrator.js +0 -399
- package/dist/pipeline/orchestrator.js.map +0 -1
- package/dist/pipeline/review-verdict.d.ts +0 -3
- package/dist/pipeline/review-verdict.d.ts.map +0 -1
- package/dist/pipeline/review-verdict.js +0 -14
- package/dist/pipeline/review-verdict.js.map +0 -1
- package/dist/pipeline/stages/code-review.d.ts +0 -35
- package/dist/pipeline/stages/code-review.d.ts.map +0 -1
- package/dist/pipeline/stages/code-review.js +0 -55
- package/dist/pipeline/stages/code-review.js.map +0 -1
- package/dist/pipeline/stages/deep-interview.d.ts +0 -15
- package/dist/pipeline/stages/deep-interview.d.ts.map +0 -1
- package/dist/pipeline/stages/deep-interview.js +0 -32
- package/dist/pipeline/stages/deep-interview.js.map +0 -1
- package/dist/pipeline/stages/ralph-verify.d.ts +0 -53
- package/dist/pipeline/stages/ralph-verify.d.ts.map +0 -1
- package/dist/pipeline/stages/ralph-verify.js +0 -89
- package/dist/pipeline/stages/ralph-verify.js.map +0 -1
- package/dist/pipeline/stages/ralplan.d.ts +0 -27
- package/dist/pipeline/stages/ralplan.d.ts.map +0 -1
- package/dist/pipeline/stages/ralplan.js +0 -152
- package/dist/pipeline/stages/ralplan.js.map +0 -1
- package/dist/pipeline/stages/team-exec.d.ts +0 -52
- package/dist/pipeline/stages/team-exec.d.ts.map +0 -1
- package/dist/pipeline/stages/team-exec.js +0 -266
- package/dist/pipeline/stages/team-exec.js.map +0 -1
- package/dist/pipeline/stages/ultragoal.d.ts +0 -23
- package/dist/pipeline/stages/ultragoal.d.ts.map +0 -1
- package/dist/pipeline/stages/ultragoal.js +0 -53
- package/dist/pipeline/stages/ultragoal.js.map +0 -1
- package/dist/pipeline/stages/ultraqa.d.ts +0 -33
- package/dist/pipeline/stages/ultraqa.d.ts.map +0 -1
- package/dist/pipeline/stages/ultraqa.js +0 -49
- package/dist/pipeline/stages/ultraqa.js.map +0 -1
- package/dist/pipeline/types.d.ts +0 -124
- package/dist/pipeline/types.d.ts.map +0 -1
- package/dist/pipeline/types.js +0 -8
- package/dist/pipeline/types.js.map +0 -1
- package/dist/ralplan/__tests__/consensus-gate.test.d.ts +0 -2
- package/dist/ralplan/__tests__/consensus-gate.test.d.ts.map +0 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +0 -1455
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +0 -1
- package/dist/ralplan/consensus-gate.d.ts +0 -62
- package/dist/ralplan/consensus-gate.d.ts.map +0 -1
- package/dist/ralplan/consensus-gate.js +0 -760
- package/dist/ralplan/consensus-gate.js.map +0 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +0 -25
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +0 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +0 -377
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +0 -1
- package/dist/state/__tests__/planning-gate.test.d.ts +0 -2
- package/dist/state/__tests__/planning-gate.test.d.ts.map +0 -1
- package/dist/state/__tests__/planning-gate.test.js +0 -629
- package/dist/state/__tests__/planning-gate.test.js.map +0 -1
- package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +0 -39
- package/plugins/oh-my-codex/skills/pipeline/SKILL.md +0 -97
- package/plugins/oh-my-codex/skills/prometheus-strict/README.md +0 -35
- package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +0 -219
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +0 -298
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +0 -190
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +0 -62
- package/prompts/prometheus-strict-metis.md +0 -274
- package/prompts/prometheus-strict-momus.md +0 -82
- package/prompts/prometheus-strict-oracle.md +0 -107
- package/prompts/scholastic.md +0 -11
- package/prompts/sisyphus-lite.md +0 -111
- package/skills/ask-claude/SKILL.md +0 -12
- package/skills/ask-gemini/SKILL.md +0 -12
- package/skills/build-fix/SKILL.md +0 -10
- package/skills/deepsearch/SKILL.md +0 -10
- package/skills/ecomode/SKILL.md +0 -114
- package/skills/ecomode/references/agent-tiers.md +0 -61
- package/skills/frontend-ui-ux/SKILL.md +0 -16
- package/skills/help/SKILL.md +0 -10
- package/skills/note/SKILL.md +0 -10
- package/skills/prometheus-strict/README.md +0 -35
- package/skills/prometheus-strict/SKILL.md +0 -219
- package/skills/ralph-init/SKILL.md +0 -10
- package/skills/review/SKILL.md +0 -10
- package/skills/security-review/SKILL.md +0 -10
- package/skills/swarm/SKILL.md +0 -12
- package/skills/tdd/SKILL.md +0 -104
- package/skills/trace/SKILL.md +0 -10
- package/skills/visual-verdict/SKILL.md +0 -10
- package/skills/web-clone/SKILL.md +0 -357
- package/src/scripts/notify-hook/ralph-session-resume.ts +0 -466
package/dist/cli/index.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Multi-agent orchestration for OpenAI Codex CLI
|
|
4
4
|
*/
|
|
5
5
|
import { execFileSync, spawn } from "child_process";
|
|
6
|
-
import { basename, dirname, join, posix, resolve, win32 } from "path";
|
|
7
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "fs";
|
|
8
|
-
import { copyFile, cp, lstat, mkdir, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, win32 } from "path";
|
|
7
|
+
import { appendFileSync, chmodSync, closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
8
|
+
import { copyFile, cp, lstat, mkdir, open, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
|
|
9
9
|
import { constants as osConstants, homedir } from "os";
|
|
10
10
|
import { createHash, randomUUID } from "crypto";
|
|
11
11
|
import { setup, SETUP_MCP_MODES, SETUP_SCOPES, SETUP_TEAM_MODES, } from "./setup.js";
|
|
@@ -18,6 +18,7 @@ import { sidecarCommand } from "../sidecar/index.js";
|
|
|
18
18
|
import { teamCommand } from "./team.js";
|
|
19
19
|
import { ralphCommand } from "./ralph.js";
|
|
20
20
|
import { ralplanCommand } from "./ralplan.js";
|
|
21
|
+
import { autopilotCommand } from "./autopilot.js";
|
|
21
22
|
import { ultragoalCommand } from "./ultragoal.js";
|
|
22
23
|
import { performanceGoalCommand } from "./performance-goal.js";
|
|
23
24
|
import { askCommand } from "./ask.js";
|
|
@@ -41,7 +42,7 @@ import { authCommand } from "./auth.js";
|
|
|
41
42
|
import { missionCommand } from "./mission.js";
|
|
42
43
|
import { resolveCodexGlobalOptionValue, runAuthHotswap, } from "../auth/hotswap.js";
|
|
43
44
|
import { MADMAX_FLAG, CODEX_BYPASS_FLAG, HIGH_REASONING_FLAG, XHIGH_REASONING_FLAG, SPARK_FLAG, MADMAX_SPARK_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, } from "./constants.js";
|
|
44
|
-
import { getBaseStateDir, getBaseStateDirWithSource, getStateDir, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
|
|
45
|
+
import { getBaseStateDir, getBaseStateDirWithSource, getStateDir, isModeStateFilename, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
|
|
45
46
|
import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
|
|
46
47
|
import { normalizeTerminalWorkflowState } from "../state/terminal-normalization.js";
|
|
47
48
|
import { readPersistedSetupPreferences, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
|
|
@@ -50,14 +51,14 @@ import { discoverOmxPluginCacheDirs, hasLocalOmxPluginEnablement, materializePac
|
|
|
50
51
|
import { escapeTomlString, readTopLevelTomlString, upsertTopLevelTomlString } from "../utils/toml.js";
|
|
51
52
|
import { ROOT_REASONING_EFFORTS, isUnsupportedRootReasoningEffort, normalizeUnsupportedRootReasoningEffort, } from "../config/models.js";
|
|
52
53
|
export { readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
|
|
53
|
-
import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath,
|
|
54
|
+
import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath, listActiveSkills, syncCanonicalSkillStateForMode, updateRootSkillActiveStateForStateDir, } from "../state/skill-active.js";
|
|
54
55
|
import { isTrackedWorkflowMode } from "../state/workflow-transition.js";
|
|
55
56
|
import { maybeCheckAndPromptUpdate, runImmediateUpdate } from "./update.js";
|
|
56
57
|
import { maybePromptGithubStar } from "./star-prompt.js";
|
|
57
58
|
import { generateOverlay, removeSessionModelInstructionsFile, resolveSessionOrchestrationMode, sessionModelInstructionsPath, writeSessionModelInstructionsFile, } from "../hooks/agents-overlay.js";
|
|
58
|
-
import {
|
|
59
|
+
import { closeLaunchSessionBindingOnce, establishLaunchSessionBinding, readNativeSessionOwnerEvidence, readSessionPointer, readSessionState, resolveSessionPointerContext, finalizeBoundOnce, isSessionPointerLaunchAbort, normalizeSessionId, resetSessionMetrics, } from "../hooks/session.js";
|
|
59
60
|
import { probeActualTmuxInstanceEvidence, tmuxEvidenceBindsCandidate } from "../scripts/notify-hook/managed-tmux.js";
|
|
60
|
-
import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable,
|
|
61
|
+
import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, } from "../team/tmux-session.js";
|
|
61
62
|
import { getPackageRoot } from "../utils/package.js";
|
|
62
63
|
import { codexConfigPath, omxRoot, rememberOmxLaunchContext, resolveOmxCliEntryPath } from "../utils/paths.js";
|
|
63
64
|
import { cleanCodexModelAvailabilityNuxIfNeeded, extractSharedMcpRegistryServersFromConfig, repairConfigIfNeeded, repairProjectScopeTrustStateForLaunch, syncProjectScopeTrustStateFromRuntime } from "../config/generator.js";
|
|
@@ -73,6 +74,7 @@ import { buildHookEvent } from "../hooks/extensibility/events.js";
|
|
|
73
74
|
import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
|
|
74
75
|
import { collectInheritableTeamWorkerArgs as collectInheritableTeamWorkerArgsShared, parseTeamWorkerLaunchArgs, resolveTeamWorkerLaunchArgs, resolveTeamLowComplexityDefaultModel, serializeTeamWorkerLaunchArgs, TEAM_WORKER_INHERITED_MODEL_ENV, } from "../team/model-contract.js";
|
|
75
76
|
import { parseWorktreeMode, planWorktreeTarget, ensureWorktree, } from "../team/worktree.js";
|
|
77
|
+
import { resolveVerifiedDetachedTeamContext } from "../team/state-root.js";
|
|
76
78
|
import { ensureReusableNodeModules } from "../utils/repo-deps.js";
|
|
77
79
|
import { resolveWorktreeToolContext, worktreeToolContextEnv } from "../utils/worktree-tool-context.js";
|
|
78
80
|
import { OMX_NOTIFY_TEMP_CONTRACT_ENV, parseNotifyTempContractFromArgs, serializeNotifyTempContract, } from "../notifications/temp-contract.js";
|
|
@@ -123,7 +125,7 @@ Usage:
|
|
|
123
125
|
omx explore DEPRECATED compatibility command; use normal repo inspection or omx sparkshell
|
|
124
126
|
omx api Run native omx-api localhost gateway commands (serve|status|stop|generate)
|
|
125
127
|
omx session Search and summarize local session history (--codex-home <path> escape hatch)
|
|
126
|
-
Includes session lock
|
|
128
|
+
Includes session lock diagnostics and verified-dead pointer recovery
|
|
127
129
|
omx url Passive URL reader (read <url> --json)
|
|
128
130
|
omx capabilities
|
|
129
131
|
Lock/check deterministic configured tool, skill, agent, and observation surfaces
|
|
@@ -134,7 +136,8 @@ Usage:
|
|
|
134
136
|
Alias for agents-init (lightweight AGENTS bootstrap only)
|
|
135
137
|
omx team Spawn parallel worker panes in tmux and bootstrap inbox/task state
|
|
136
138
|
omx ralph Launch Codex with ralph persistence mode active
|
|
137
|
-
omx ralplan
|
|
139
|
+
omx ralplan Adapted Ralplan authority support; preflight applies only when native role routing is unavailable
|
|
140
|
+
and adapted Ralplan authority is requested; ordinary work remains under its own workflow gates
|
|
138
141
|
omx ultragoal Create, resume, and checkpoint durable multi-goal plans over Codex goal mode
|
|
139
142
|
omx performance-goal
|
|
140
143
|
Create, hand off, and gate evaluator-backed performance goals
|
|
@@ -196,6 +199,9 @@ Options:
|
|
|
196
199
|
Clear the persisted AGENTS merge policy for this project root
|
|
197
200
|
--dry-run Show what would be done without doing it
|
|
198
201
|
--plugin Use Codex plugin delivery for omx setup and remove legacy OMX-managed user/project components
|
|
202
|
+
--disable-hooks
|
|
203
|
+
Disable only OMX-owned hook registrations and related enablement; preserve foreign hooks and .omx artifacts
|
|
204
|
+
--repair-state Archive stale state projections under .omx/archive/ during omx doctor
|
|
199
205
|
--legacy Use legacy setup delivery for omx setup, overriding persisted plugin mode
|
|
200
206
|
--install-mode <legacy|plugin>
|
|
201
207
|
Explicit setup install mode (canonical form; --legacy/--plugin are aliases)
|
|
@@ -262,7 +268,6 @@ const ALLOWED_SHELLS = new Set([
|
|
|
262
268
|
"/opt/local/bin/zsh",
|
|
263
269
|
"/opt/homebrew/bin/zsh",
|
|
264
270
|
]);
|
|
265
|
-
const WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS = 2500;
|
|
266
271
|
const CODEX_VERSION_FLAGS = new Set(["--version", "-V"]);
|
|
267
272
|
const TMUX_EXTENDED_KEYS_MODE = "always";
|
|
268
273
|
const TMUX_EXTENDED_KEYS_FALLBACK_MODE = "off";
|
|
@@ -270,6 +275,7 @@ const TMUX_EXTENDED_KEYS_LEASE_DIR = "tmux-extended-keys";
|
|
|
270
275
|
const TMUX_EXTENDED_KEYS_LOCK_RETRY_MS = 20;
|
|
271
276
|
const TMUX_EXTENDED_KEYS_LOCK_MAX_ATTEMPTS = 100;
|
|
272
277
|
const TMUX_EXTENDED_KEYS_LOCK_STALE_MS = 30_000;
|
|
278
|
+
export const DETACHED_LEADER_READY_TIMEOUT_MS = 120_000;
|
|
273
279
|
const RESUME_HELP = `Usage: omx resume [--project] [--codex-home <path>] [codex resume options]
|
|
274
280
|
|
|
275
281
|
Read-only help does not prepare or launch a Codex session.`;
|
|
@@ -520,6 +526,9 @@ export function resolveCliInvocation(args) {
|
|
|
520
526
|
if (firstArg === "resume") {
|
|
521
527
|
return { command: "resume", launchArgs: args.slice(1) };
|
|
522
528
|
}
|
|
529
|
+
if (firstArg === "__detached-session-leader") {
|
|
530
|
+
return { command: firstArg, launchArgs: args.slice(1) };
|
|
531
|
+
}
|
|
523
532
|
return { command: firstArg, launchArgs: [] };
|
|
524
533
|
}
|
|
525
534
|
export function resolveUpdateChannelArg(args) {
|
|
@@ -823,6 +832,10 @@ export async function prepareRuntimeCodexHomeForProjectLaunch(cwd, sessionId, pr
|
|
|
823
832
|
await writeFile(destination, launchConfig, "utf-8");
|
|
824
833
|
continue;
|
|
825
834
|
}
|
|
835
|
+
if (entry.name === "plugins" && entry.isDirectory()) {
|
|
836
|
+
await cp(source, destination, { recursive: true });
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
826
839
|
await linkOrCopyCodexHomeEntry(source, destination);
|
|
827
840
|
}
|
|
828
841
|
await ensureProjectLaunchRuntimeHistoryLinks(runtimeCodexHome, projectCodexHome);
|
|
@@ -1042,6 +1055,247 @@ function execTmuxFileSync(args, options) {
|
|
|
1042
1055
|
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
1043
1056
|
});
|
|
1044
1057
|
}
|
|
1058
|
+
function captureDetachedLeaderAuthority(leaderPaneId, expectedSessionName, ownerId) {
|
|
1059
|
+
if (!/^%[0-9]+$/.test(leaderPaneId) || !ownerId)
|
|
1060
|
+
throw new Error("invalid detached leader authority target");
|
|
1061
|
+
const output = execTmuxFileSync([
|
|
1062
|
+
"display-message", "-p", "-t", leaderPaneId,
|
|
1063
|
+
"#{session_name}\t#{session_id}\t#{session_created}\t#{window_index}\t#{window_id}\t#{pane_id}\t#{pane_pid}",
|
|
1064
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1065
|
+
const fields = output.split("\t");
|
|
1066
|
+
if (fields.length !== 7)
|
|
1067
|
+
throw new Error("malformed detached leader authority");
|
|
1068
|
+
const [sessionName, sessionId, sessionCreated, windowIndex, windowId, paneId, panePid] = fields;
|
|
1069
|
+
if (sessionName !== expectedSessionName || paneId !== leaderPaneId || !/^\$[0-9]+$/.test(sessionId)
|
|
1070
|
+
|| !/^[0-9]+$/.test(sessionCreated) || !/^[0-9]+$/.test(windowIndex)
|
|
1071
|
+
|| !/^@[0-9]+$/.test(windowId) || !/^[1-9][0-9]*$/.test(panePid)) {
|
|
1072
|
+
throw new Error("malformed detached leader authority");
|
|
1073
|
+
}
|
|
1074
|
+
const parsedPid = Number(panePid);
|
|
1075
|
+
if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
|
|
1076
|
+
throw new Error("malformed detached leader authority");
|
|
1077
|
+
return { paneId, panePid: parsedPid, sessionName, sessionId, sessionCreated, windowId, windowIndex, ownerId };
|
|
1078
|
+
}
|
|
1079
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
1080
|
+
export function parseDetachedLeaderPaneIdByPid(snapshot, leaderPid) {
|
|
1081
|
+
if (!Number.isSafeInteger(leaderPid) || leaderPid <= 0)
|
|
1082
|
+
throw new Error("invalid detached leader pid");
|
|
1083
|
+
const matches = snapshot
|
|
1084
|
+
.split("\n")
|
|
1085
|
+
.map((line) => line.trim())
|
|
1086
|
+
.filter(Boolean)
|
|
1087
|
+
.map((line) => line.split("\t"))
|
|
1088
|
+
.filter(([paneId, paneDead, panePid]) => /^%[0-9]+$/.test(paneId ?? "") && paneDead === "0" && Number(panePid) === leaderPid)
|
|
1089
|
+
.map(([paneId]) => paneId);
|
|
1090
|
+
if (matches.length !== 1)
|
|
1091
|
+
throw new Error("detached leader pane identity is unavailable");
|
|
1092
|
+
return matches[0];
|
|
1093
|
+
}
|
|
1094
|
+
function detachedLeaderAuthorityCondition(authority, requireOwner = true) {
|
|
1095
|
+
const conditions = [
|
|
1096
|
+
"#{==:#{pane_dead},0}",
|
|
1097
|
+
`#{==:#{pane_id},${authority.paneId}}`,
|
|
1098
|
+
`#{==:#{pane_pid},${authority.panePid}}`,
|
|
1099
|
+
`#{==:#{session_name},${authority.sessionName}}`,
|
|
1100
|
+
`#{==:#{session_id},${authority.sessionId}}`,
|
|
1101
|
+
`#{==:#{session_created},${authority.sessionCreated}}`,
|
|
1102
|
+
`#{==:#{window_id},${authority.windowId}}`,
|
|
1103
|
+
...(requireOwner ? [`#{==:#{@omx_instance_id},${authority.ownerId}}`] : []),
|
|
1104
|
+
];
|
|
1105
|
+
return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
1106
|
+
}
|
|
1107
|
+
function detachedHudAuthorityCondition(authority, requireMarker = true) {
|
|
1108
|
+
const conditions = [
|
|
1109
|
+
"#{==:#{pane_dead},0}",
|
|
1110
|
+
`#{==:#{pane_id},${authority.paneId}}`,
|
|
1111
|
+
`#{==:#{pane_pid},${authority.panePid}}`,
|
|
1112
|
+
`#{==:#{session_id},${authority.sessionId}}`,
|
|
1113
|
+
`#{==:#{window_id},${authority.windowId}}`,
|
|
1114
|
+
...(requireMarker ? [`#{m:*OMX_DETACHED_HUD_OPERATION=${authority.operationMarker}*,#{pane_start_command}}`] : []),
|
|
1115
|
+
];
|
|
1116
|
+
return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
1117
|
+
}
|
|
1118
|
+
function detachedAuthorityReceipt() {
|
|
1119
|
+
return `omx_detached_${randomUUID()}`;
|
|
1120
|
+
}
|
|
1121
|
+
function describeDetachedLeaderAuthorityMismatch(authority) {
|
|
1122
|
+
const output = execTmuxFileSync([
|
|
1123
|
+
"display-message", "-p", "-t", authority.paneId,
|
|
1124
|
+
"#{pane_dead}\t#{pane_id}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{window_id}\t#{@omx_instance_id}",
|
|
1125
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1126
|
+
const observed = output.split("\t");
|
|
1127
|
+
if (observed.length !== 8)
|
|
1128
|
+
return "authority observation was malformed";
|
|
1129
|
+
const expected = ["0", authority.paneId, String(authority.panePid), authority.sessionName, authority.sessionId, authority.sessionCreated, authority.windowId, authority.ownerId];
|
|
1130
|
+
const labels = ["pane_dead", "pane_id", "pane_pid", "session_name", "session_id", "session_created", "window_id", "owner"];
|
|
1131
|
+
const mismatches = observed.flatMap((value, index) => value === expected[index] ? [] : [`${labels[index]} expected ${JSON.stringify(expected[index])} observed ${JSON.stringify(value)}`]);
|
|
1132
|
+
return mismatches.length > 0 ? mismatches.join(", ") : "authority condition was not observable";
|
|
1133
|
+
}
|
|
1134
|
+
function runDetachedLeaderMutation(authority, args, requireOwner = true, retryMissingReceipt = false) {
|
|
1135
|
+
const attempts = retryMissingReceipt ? 2 : 1;
|
|
1136
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1137
|
+
const receipt = detachedAuthorityReceipt();
|
|
1138
|
+
const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
|
|
1139
|
+
const output = execTmuxFileSync([
|
|
1140
|
+
"if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority, requireOwner),
|
|
1141
|
+
success, "display-message -p ''",
|
|
1142
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1143
|
+
if (output === receipt)
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const mutation = args.length >= 4 && args[0] === "set-option" ? `${args[0]} ${args.at(-2)}` : args[0] ?? "unknown";
|
|
1147
|
+
throw new Error(`detached leader authority blocked tmux mutation ${mutation}: ${describeDetachedLeaderAuthorityMismatch(authority)}`);
|
|
1148
|
+
}
|
|
1149
|
+
function detachedPreReportCleanupCondition(authority) {
|
|
1150
|
+
const conditions = [
|
|
1151
|
+
"#{==:#{pane_dead},1}",
|
|
1152
|
+
`#{==:#{pane_id},${authority.paneId}}`,
|
|
1153
|
+
`#{==:#{session_name},${authority.sessionName}}`,
|
|
1154
|
+
`#{==:#{session_id},${authority.sessionId}}`,
|
|
1155
|
+
`#{==:#{session_created},${authority.sessionCreated}}`,
|
|
1156
|
+
`#{==:#{window_id},${authority.windowId}}`,
|
|
1157
|
+
];
|
|
1158
|
+
return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
1159
|
+
}
|
|
1160
|
+
export function cleanupDetachedPreReportSession(authority) {
|
|
1161
|
+
const receipt = detachedAuthorityReceipt();
|
|
1162
|
+
const success = `kill-session -t ${quoteShellArg(authority.sessionName)} ; display-message -p ${quoteShellArg(receipt)}`;
|
|
1163
|
+
const output = execTmuxFileSync([
|
|
1164
|
+
"if-shell", "-F", "-t", authority.paneId, detachedPreReportCleanupCondition(authority),
|
|
1165
|
+
success, "display-message -p ''",
|
|
1166
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1167
|
+
if (output !== receipt)
|
|
1168
|
+
throw new Error("detached pre-report topology changed before cleanup");
|
|
1169
|
+
}
|
|
1170
|
+
function runDetachedHudMutation(leaderAuthority, hudAuthority, args, requireMarker = true) {
|
|
1171
|
+
const receipt = detachedAuthorityReceipt();
|
|
1172
|
+
const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
|
|
1173
|
+
const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(detachedHudAuthorityCondition(hudAuthority, requireMarker))} ${quoteShellArg(success)} ${quoteShellArg("display-message -p ''")}`;
|
|
1174
|
+
const output = execTmuxFileSync([
|
|
1175
|
+
"if-shell", "-F", "-t", leaderAuthority.paneId, detachedLeaderAuthorityCondition(leaderAuthority),
|
|
1176
|
+
hudGuard, "display-message -p ''",
|
|
1177
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1178
|
+
if (output !== receipt)
|
|
1179
|
+
throw new Error("detached leader or HUD authority changed before tmux mutation");
|
|
1180
|
+
}
|
|
1181
|
+
export function cleanupDetachedHudPane(authority, ownerId) {
|
|
1182
|
+
if (ownerId) {
|
|
1183
|
+
execTmuxFileSync(["kill-pane", "-t", authority.paneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
execTmuxFileSync([
|
|
1187
|
+
"if-shell", "-F", "-t", authority.paneId, detachedHudAuthorityCondition(authority, false),
|
|
1188
|
+
`kill-pane -t ${quoteShellArg(authority.paneId)}`, "",
|
|
1189
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1190
|
+
}
|
|
1191
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
1192
|
+
const panes = execTmuxFileSync(["list-panes", "-a", "-F", "#{pane_id}"], {
|
|
1193
|
+
encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"],
|
|
1194
|
+
}).split("\n").map((value) => value.trim()).filter(Boolean);
|
|
1195
|
+
if (!panes.includes(authority.paneId))
|
|
1196
|
+
return;
|
|
1197
|
+
blockMs(20);
|
|
1198
|
+
}
|
|
1199
|
+
throw new Error("detached HUD topology changed before teardown");
|
|
1200
|
+
}
|
|
1201
|
+
function tagDetachedHudPane(leaderAuthority, authority, ownerId) {
|
|
1202
|
+
runDetachedHudMutation(leaderAuthority, authority, [
|
|
1203
|
+
"set-option", "-pq", "-t", authority.paneId, "@omx_hud_owner", ownerId,
|
|
1204
|
+
], false);
|
|
1205
|
+
}
|
|
1206
|
+
export function discoverDetachedHudAuthority(sessionName, ownerId) {
|
|
1207
|
+
const rows = execTmuxFileSync([
|
|
1208
|
+
"list-panes", "-a", "-F",
|
|
1209
|
+
"#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{window_id}\t#{@omx_hud_owner}",
|
|
1210
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1211
|
+
const matches = rows.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
1212
|
+
const [paneId, paneDead, panePidRaw, discoveredSessionName, sessionId, sessionCreated, windowId, owner] = line.split("\t");
|
|
1213
|
+
const panePid = Number(panePidRaw);
|
|
1214
|
+
if (paneDead !== "0" || discoveredSessionName !== sessionName || owner !== ownerId || !/^%\d+$/.test(paneId ?? "") || !/^\$\d+$/.test(sessionId ?? "") || !/^\d+$/.test(sessionCreated ?? "") || !/^@\d+$/.test(windowId ?? "") || !Number.isSafeInteger(panePid) || panePid <= 0)
|
|
1215
|
+
return [];
|
|
1216
|
+
return [{ paneId: paneId, panePid, sessionId: sessionId, sessionCreated: sessionCreated, windowId: windowId, operationMarker: randomUUID() }];
|
|
1217
|
+
});
|
|
1218
|
+
if (matches.length > 1)
|
|
1219
|
+
throw new Error("multiple detached HUD ownership tags found");
|
|
1220
|
+
return matches[0];
|
|
1221
|
+
}
|
|
1222
|
+
function buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, command) {
|
|
1223
|
+
const deferredReceipt = detachedAuthorityReceipt();
|
|
1224
|
+
let guardedSinkCount = 0;
|
|
1225
|
+
const guardedResize = (_match, sink) => {
|
|
1226
|
+
guardedSinkCount += 1;
|
|
1227
|
+
// The stored hook payload is consumed by one outer tmux format pass when
|
|
1228
|
+
// run-shell executes it. The injected authority conditions must survive that
|
|
1229
|
+
// pass so they are evaluated at execution time against the guarded panes;
|
|
1230
|
+
// unescaped, tmux pre-expands them against the hook's own pane and collapses
|
|
1231
|
+
// them to constants, denying the resize before the sink is parsed (#3292).
|
|
1232
|
+
const outerFormatEscaped = (condition) => condition.replaceAll("#{", "##{");
|
|
1233
|
+
const success = `${sink} ; display-message -p ${quoteShellArg(deferredReceipt)}`;
|
|
1234
|
+
const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedHudAuthorityCondition(hudAuthority)))} ${quoteShellArg(success)} ${quoteShellArg("")}`;
|
|
1235
|
+
return `tmux if-shell -F -t ${quoteShellArg(leaderAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedLeaderAuthorityCondition(leaderAuthority)))} ${quoteShellArg(hudGuard)} ${quoteShellArg("")}`;
|
|
1236
|
+
};
|
|
1237
|
+
const outerFormatEscapedCommand = command
|
|
1238
|
+
.replaceAll('#{pane_id}', '##{pane_id}')
|
|
1239
|
+
.replaceAll('#{pane_dead}', '##{pane_dead}')
|
|
1240
|
+
.replaceAll('#{pane_pid}', '##{pane_pid}');
|
|
1241
|
+
const guardedCommand = outerFormatEscapedCommand.replace(/tmux (resize-pane -t %[0-9]+ -y [1-9][0-9]*)/g, guardedResize);
|
|
1242
|
+
if (guardedSinkCount === 0) {
|
|
1243
|
+
throw new Error("detached deferred HUD mutation lacks a recognized resize sink");
|
|
1244
|
+
}
|
|
1245
|
+
return guardedCommand;
|
|
1246
|
+
}
|
|
1247
|
+
export function guardDetachedHudDeferredMutation(leaderAuthority, hudAuthority, args) {
|
|
1248
|
+
if (args[0] === "set-hook" && typeof args.at(-1) === "string") {
|
|
1249
|
+
const hookCommand = args.at(-1);
|
|
1250
|
+
const runShellPrefix = "run-shell -b ";
|
|
1251
|
+
if (!hookCommand.startsWith(runShellPrefix)) {
|
|
1252
|
+
throw new Error("detached deferred HUD hook lacks a run-shell command");
|
|
1253
|
+
}
|
|
1254
|
+
const quotedPayload = hookCommand.slice(runShellPrefix.length);
|
|
1255
|
+
if (!quotedPayload.startsWith("'") || !quotedPayload.endsWith("'")) {
|
|
1256
|
+
throw new Error("detached deferred HUD hook has an invalid run-shell payload");
|
|
1257
|
+
}
|
|
1258
|
+
const payload = quotedPayload
|
|
1259
|
+
.slice(1, -1)
|
|
1260
|
+
.replaceAll(`'"'"'`, "'")
|
|
1261
|
+
.replaceAll("'\\''", "'");
|
|
1262
|
+
const guardedPayload = buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, payload);
|
|
1263
|
+
return [...args.slice(0, -1), `${runShellPrefix}${quoteShellArg(guardedPayload)}`];
|
|
1264
|
+
}
|
|
1265
|
+
if (args[0] === "run-shell") {
|
|
1266
|
+
const commandIndex = args.length - 1;
|
|
1267
|
+
return [...args.slice(0, commandIndex), buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, args[commandIndex])];
|
|
1268
|
+
}
|
|
1269
|
+
return args;
|
|
1270
|
+
}
|
|
1271
|
+
function runDetachedLeaderSplit(authority, args) {
|
|
1272
|
+
const receipt = detachedAuthorityReceipt();
|
|
1273
|
+
const operationMarker = randomUUID();
|
|
1274
|
+
const splitArgs = [...args];
|
|
1275
|
+
const targetIndex = splitArgs.indexOf("-t");
|
|
1276
|
+
if (targetIndex < 0 || splitArgs[targetIndex + 1] !== authority.sessionName)
|
|
1277
|
+
throw new Error("invalid detached HUD split target");
|
|
1278
|
+
splitArgs[targetIndex + 1] = authority.paneId;
|
|
1279
|
+
const formatIndex = splitArgs.indexOf("-F");
|
|
1280
|
+
if (formatIndex < 0 || splitArgs[formatIndex + 1] !== "#{pane_id}")
|
|
1281
|
+
throw new Error("invalid detached HUD split receipt format");
|
|
1282
|
+
const commandIndex = splitArgs.length - 1;
|
|
1283
|
+
splitArgs[commandIndex] = `env OMX_DETACHED_HUD_OPERATION=${operationMarker} ${splitArgs[commandIndex]}`;
|
|
1284
|
+
splitArgs[formatIndex + 1] = `#{pane_id}\t#{pane_pid}\t#{session_id}\t#{window_id}\t${receipt}`;
|
|
1285
|
+
const output = execTmuxFileSync([
|
|
1286
|
+
"if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority),
|
|
1287
|
+
splitArgs.map(quoteShellArg).join(" "), "display-message -p ''",
|
|
1288
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1289
|
+
const [paneId, panePid, sessionId, windowId, observedReceipt, ...extra] = output.split("\t");
|
|
1290
|
+
if (!/^%[0-9]+$/.test(paneId) || !/^[1-9][0-9]*$/.test(panePid) || !/^\$[0-9]+$/.test(sessionId)
|
|
1291
|
+
|| !/^@[0-9]+$/.test(windowId) || observedReceipt !== receipt || extra.length !== 0) {
|
|
1292
|
+
throw new Error("detached leader authority changed before HUD split");
|
|
1293
|
+
}
|
|
1294
|
+
const parsedPid = Number(panePid);
|
|
1295
|
+
if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
|
|
1296
|
+
throw new Error("malformed detached HUD authority");
|
|
1297
|
+
return { paneId, panePid: parsedPid, sessionId, windowId, operationMarker };
|
|
1298
|
+
}
|
|
1045
1299
|
function readTmuxEnvValueForTarget(targetPaneId) {
|
|
1046
1300
|
if (!targetPaneId.startsWith("%"))
|
|
1047
1301
|
return undefined;
|
|
@@ -1129,29 +1383,6 @@ function clearDetachedTmuxSessionHistoryIfUnattached(sessionName, leaderPaneId)
|
|
|
1129
1383
|
logCliOperationFailure(err);
|
|
1130
1384
|
}
|
|
1131
1385
|
}
|
|
1132
|
-
function readTmuxSessionInstanceId(sessionName) {
|
|
1133
|
-
try {
|
|
1134
|
-
return execTmuxFileSync(["show-options", "-qv", "-t", sessionName, OMX_INSTANCE_OPTION], {
|
|
1135
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1136
|
-
encoding: "utf-8",
|
|
1137
|
-
}).trim();
|
|
1138
|
-
}
|
|
1139
|
-
catch {
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
function tmuxPaneBelongsToSession(paneId, sessionName) {
|
|
1144
|
-
try {
|
|
1145
|
-
const paneSessionName = execTmuxFileSync(["display-message", "-p", "-t", paneId, "#{session_name}"], {
|
|
1146
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1147
|
-
encoding: "utf-8",
|
|
1148
|
-
}).trim();
|
|
1149
|
-
return paneSessionName === sessionName;
|
|
1150
|
-
}
|
|
1151
|
-
catch {
|
|
1152
|
-
return false;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
1386
|
function buildDetachedHistoryPruneHookCommand(leaderPaneId) {
|
|
1156
1387
|
// The leader pane can be gone by the time the hook fires (e.g. crashed
|
|
1157
1388
|
// leader with a lingering session); suppress errors so tmux does not queue
|
|
@@ -1193,6 +1424,12 @@ function logCliOperationFailure(error) {
|
|
|
1193
1424
|
process.stderr.write(`[cli/index] operation failed: ${error}
|
|
1194
1425
|
`);
|
|
1195
1426
|
}
|
|
1427
|
+
function logEstablishmentCleanupEvidence(error) {
|
|
1428
|
+
const cleanup = error?.establishmentCleanup;
|
|
1429
|
+
if (!cleanup)
|
|
1430
|
+
return;
|
|
1431
|
+
process.stderr.write(`[omx] establishment cleanup evidence: ${JSON.stringify(cleanup)}\n`);
|
|
1432
|
+
}
|
|
1196
1433
|
function tmuxFailureMessage(error) {
|
|
1197
1434
|
if (!error || typeof error !== "object")
|
|
1198
1435
|
return String(error);
|
|
@@ -1207,77 +1444,18 @@ function isUnsupportedTmuxExtendedKeysFailure(error) {
|
|
|
1207
1444
|
return (message.includes("extended-keys") &&
|
|
1208
1445
|
/(?:invalid|unknown|unsupported) (?:option|flag|argument)|no such option|unknown option/.test(message));
|
|
1209
1446
|
}
|
|
1210
|
-
function
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1218
|
-
encoding: "utf-8",
|
|
1219
|
-
});
|
|
1220
|
-
return { usable: true };
|
|
1221
|
-
}
|
|
1222
|
-
catch (err) {
|
|
1223
|
-
const reason = tmuxFailureMessage(err);
|
|
1224
|
-
if (isBenignMissingTmuxServerMessage(reason)) {
|
|
1225
|
-
return { usable: true };
|
|
1226
|
-
}
|
|
1227
|
-
return { usable: false, reason };
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
function warnDetachedTmuxFallback(reason) {
|
|
1231
|
-
const suffix = reason ? ` (${reason})` : "";
|
|
1232
|
-
console.warn(`[omx] warning: tmux is installed but its server/socket is unusable${suffix}. Falling back to direct Codex launch.`);
|
|
1233
|
-
}
|
|
1234
|
-
const QUICK_ATTACH_NOOP_THRESHOLD_MS = 2_000;
|
|
1235
|
-
function isWslWindowsTerminalEnvironment(env) {
|
|
1236
|
-
return Boolean(env.WT_SESSION?.trim() &&
|
|
1237
|
-
(env.WSL_INTEROP?.trim() ||
|
|
1238
|
-
env.WSL_DISTRO_NAME?.trim() ||
|
|
1239
|
-
env.WSLENV?.trim()));
|
|
1240
|
-
}
|
|
1241
|
-
function readDetachedSessionAttachedClientCount(sessionName) {
|
|
1242
|
-
try {
|
|
1243
|
-
const output = execTmuxFileSync(["display-message", "-p", "-t", sessionName, "#{session_attached}"], {
|
|
1244
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1245
|
-
encoding: "utf-8",
|
|
1246
|
-
}).trim();
|
|
1247
|
-
const parsed = Number.parseInt(output, 10);
|
|
1248
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
1249
|
-
}
|
|
1250
|
-
catch (err) {
|
|
1251
|
-
logCliOperationFailure(err);
|
|
1252
|
-
return null;
|
|
1447
|
+
function probeDetachedTmuxTransport() {
|
|
1448
|
+
const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
|
|
1449
|
+
encoding: "utf-8",
|
|
1450
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1451
|
+
});
|
|
1452
|
+
if (result.error || result.status !== 0) {
|
|
1453
|
+
throw new DetachedTransportUnavailable();
|
|
1253
1454
|
}
|
|
1254
1455
|
}
|
|
1255
|
-
function assertDetachedAttachDidNotNoop(sessionName, elapsedMs, env) {
|
|
1256
|
-
if (!isWslWindowsTerminalEnvironment(env))
|
|
1257
|
-
return;
|
|
1258
|
-
if (elapsedMs >= QUICK_ATTACH_NOOP_THRESHOLD_MS)
|
|
1259
|
-
return;
|
|
1260
|
-
const attachedClients = readDetachedSessionAttachedClientCount(sessionName);
|
|
1261
|
-
if (attachedClients === null || attachedClients > 0)
|
|
1262
|
-
return;
|
|
1263
|
-
throw new Error([
|
|
1264
|
-
"tmux attach-session returned immediately without attaching a client",
|
|
1265
|
-
`(session=${sessionName}).`,
|
|
1266
|
-
"This can happen on WSL2 under Windows Terminal.",
|
|
1267
|
-
"Falling back to direct Codex launch.",
|
|
1268
|
-
].join(" "));
|
|
1269
|
-
}
|
|
1270
1456
|
function resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows) {
|
|
1271
|
-
const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform,
|
|
1272
|
-
|
|
1273
|
-
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1274
|
-
}
|
|
1275
|
-
const tmuxHealth = checkDetachedTmuxLaunchHealth();
|
|
1276
|
-
if (tmuxHealth.usable) {
|
|
1277
|
-
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1278
|
-
}
|
|
1279
|
-
warnDetachedTmuxFallback(tmuxHealth.reason);
|
|
1280
|
-
return { launchPolicy: "direct", effectiveExplicitLaunchPolicy: "direct" };
|
|
1457
|
+
const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, Boolean(resolveTmuxBinaryForPlatform()), nativeWindows, undefined, undefined, explicitLaunchPolicy);
|
|
1458
|
+
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1281
1459
|
}
|
|
1282
1460
|
export function resolveSignalExitCode(signal) {
|
|
1283
1461
|
if (!signal)
|
|
@@ -1507,15 +1685,6 @@ export function resolveOmxRootForLaunch(cwd, env = process.env) {
|
|
|
1507
1685
|
function resolveLaunchPath(cwd, raw) {
|
|
1508
1686
|
return isCrossPlatformAbsolutePath(raw) ? raw : join(cwd, raw);
|
|
1509
1687
|
}
|
|
1510
|
-
function resolveHudRuntimeRootSource(omxRootOverride, env = process.env) {
|
|
1511
|
-
if (env.OMX_TEAM_STATE_ROOT?.trim())
|
|
1512
|
-
return 'team-env';
|
|
1513
|
-
if (env.OMX_ROOT?.trim() || omxRootOverride)
|
|
1514
|
-
return 'omx-root-env';
|
|
1515
|
-
if (env.OMX_STATE_ROOT?.trim())
|
|
1516
|
-
return 'omx-state-root-env';
|
|
1517
|
-
return 'cwd-default';
|
|
1518
|
-
}
|
|
1519
1688
|
export function resolveHudRuntimeRootForLaunch(cwd, env = process.env) {
|
|
1520
1689
|
const omxTeamStateRoot = env.OMX_TEAM_STATE_ROOT?.trim();
|
|
1521
1690
|
if (omxTeamStateRoot) {
|
|
@@ -1574,6 +1743,8 @@ export function captureMadmaxWorktreeRuntimeContext(options) {
|
|
|
1574
1743
|
const inheritedRoot = resolveInheritedMadmaxRoot(env);
|
|
1575
1744
|
if (!inheritedRoot)
|
|
1576
1745
|
return undefined;
|
|
1746
|
+
if (!madmaxInheritedContextMatchesLaunch(options.sourceCwd, options.originalLaunchArgs, env))
|
|
1747
|
+
return undefined;
|
|
1577
1748
|
const sourceCwd = env.OMX_SOURCE_CWD?.trim() || options.sourceCwd;
|
|
1578
1749
|
const worktreeCwd = options.worktreeCwd?.trim();
|
|
1579
1750
|
const omxStateRoot = env.OMX_STATE_ROOT?.trim();
|
|
@@ -1673,7 +1844,28 @@ const MADMAX_DETACHED_LOCK_RETRY_MS = 50;
|
|
|
1673
1844
|
const MADMAX_DETACHED_LOCK_MAX_ATTEMPTS = 100;
|
|
1674
1845
|
const OMX_MADMAX_DETACHED_CONTEXT_ENV = "OMX_MADMAX_DETACHED_CONTEXT";
|
|
1675
1846
|
function resolveMadmaxRunsRoot(env = process.env) {
|
|
1676
|
-
|
|
1847
|
+
const configured = env.OMX_RUNS_DIR || join(homedir(), ".omx-runs");
|
|
1848
|
+
// Canonicalize, because the detached active-record path is derived from this root and macOS reaches
|
|
1849
|
+
// the same directory as both `/var/...` and `/private/var/...`. Without this, a launch that recorded
|
|
1850
|
+
// its active detached session under one alias could not be found under the other, so the reuse guard
|
|
1851
|
+
// silently started a DUPLICATE detached launch instead of attaching to the live one.
|
|
1852
|
+
try {
|
|
1853
|
+
return realpathSync(configured);
|
|
1854
|
+
}
|
|
1855
|
+
catch {
|
|
1856
|
+
// Not created yet: the raw path is correct for the first launch, which creates it.
|
|
1857
|
+
return configured;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
function canonicalizeExistingPath(target) {
|
|
1861
|
+
if (!target)
|
|
1862
|
+
return target;
|
|
1863
|
+
try {
|
|
1864
|
+
return realpathSync(target);
|
|
1865
|
+
}
|
|
1866
|
+
catch {
|
|
1867
|
+
return target;
|
|
1868
|
+
}
|
|
1677
1869
|
}
|
|
1678
1870
|
function canonicalizeLaunchCwd(cwd) {
|
|
1679
1871
|
try {
|
|
@@ -1681,10 +1873,14 @@ function canonicalizeLaunchCwd(cwd) {
|
|
|
1681
1873
|
cwd,
|
|
1682
1874
|
encoding: "utf-8",
|
|
1683
1875
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1684
|
-
}).trim() || cwd;
|
|
1876
|
+
}).trim() || canonicalizeExistingPath(cwd);
|
|
1685
1877
|
}
|
|
1686
1878
|
catch {
|
|
1687
|
-
|
|
1879
|
+
// Outside a git checkout realpath is the only canonical form available. Returning the raw value let
|
|
1880
|
+
// the macOS /var vs /private/var alias survive into the context key, so the same directory reached
|
|
1881
|
+
// by two aliases hashed to two different keys and a legitimate inherited context failed its own
|
|
1882
|
+
// verification.
|
|
1883
|
+
return canonicalizeExistingPath(cwd);
|
|
1688
1884
|
}
|
|
1689
1885
|
}
|
|
1690
1886
|
function normalizeMadmaxDetachedLaunchArgv(argv) {
|
|
@@ -1730,7 +1926,7 @@ export function buildMadmaxDetachedLaunchContextKey(sourceCwd, argv, runIdentity
|
|
|
1730
1926
|
const payload = JSON.stringify({
|
|
1731
1927
|
source_cwd: canonicalizeLaunchCwd(sourceCwd),
|
|
1732
1928
|
argv: normalizeMadmaxDetachedLaunchArgv(argv),
|
|
1733
|
-
run_identity: runIdentity,
|
|
1929
|
+
run_identity: canonicalizeExistingPath(runIdentity),
|
|
1734
1930
|
});
|
|
1735
1931
|
return createHash("sha256").update(payload).digest("hex").slice(0, 32);
|
|
1736
1932
|
}
|
|
@@ -1761,31 +1957,77 @@ function readMadmaxDetachedActiveRecord(recordPath) {
|
|
|
1761
1957
|
tmux_session_name: parsed.tmux_session_name,
|
|
1762
1958
|
...(typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}),
|
|
1763
1959
|
...(typeof parsed.tmux_pane_id === "string" ? { tmux_pane_id: parsed.tmux_pane_id } : {}),
|
|
1960
|
+
...(typeof parsed.tmux_internal_session_id === "string" ? { tmux_internal_session_id: parsed.tmux_internal_session_id } : {}),
|
|
1961
|
+
...(typeof parsed.tmux_session_created === "string" ? { tmux_session_created: parsed.tmux_session_created } : {}),
|
|
1962
|
+
...(typeof parsed.tmux_pane_pid === "number" ? { tmux_pane_pid: parsed.tmux_pane_pid } : {}),
|
|
1764
1963
|
...(typeof parsed.worktree_cwd === "string" ? { worktree_cwd: parsed.worktree_cwd } : {}),
|
|
1964
|
+
...(typeof parsed.launch_nonce === "string" ? { launch_nonce: parsed.launch_nonce } : {}),
|
|
1965
|
+
...(typeof parsed.leader_pid === "number" && Number.isSafeInteger(parsed.leader_pid) && parsed.leader_pid > 0 ? { leader_pid: parsed.leader_pid } : {}),
|
|
1966
|
+
...(typeof parsed.base_state_root === "string" ? { base_state_root: parsed.base_state_root } : {}),
|
|
1967
|
+
...(parsed.lifecycle_phase === "ready" || parsed.lifecycle_phase === "terminal" ? { lifecycle_phase: parsed.lifecycle_phase } : {}),
|
|
1765
1968
|
};
|
|
1766
1969
|
}
|
|
1767
1970
|
catch {
|
|
1768
1971
|
return null;
|
|
1769
1972
|
}
|
|
1770
1973
|
}
|
|
1974
|
+
function queryMadmaxDetachedRuntimeBinding(record) {
|
|
1975
|
+
if (!record.tmux_pane_id || !record.session_id)
|
|
1976
|
+
return null;
|
|
1977
|
+
try {
|
|
1978
|
+
const output = execTmuxFileSync([
|
|
1979
|
+
"list-panes", "-a", "-F",
|
|
1980
|
+
"#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{@omx_instance_id}",
|
|
1981
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1982
|
+
let binding = null;
|
|
1983
|
+
const seen = new Set();
|
|
1984
|
+
for (const line of output.split("\n").filter(Boolean)) {
|
|
1985
|
+
const fields = line.split("\t");
|
|
1986
|
+
if (fields.length !== 7)
|
|
1987
|
+
return null;
|
|
1988
|
+
const [paneId, dead, panePidRaw, sessionName, internalSessionId, sessionCreated, omxInstanceId] = fields;
|
|
1989
|
+
const panePid = Number(panePidRaw);
|
|
1990
|
+
if (!/^%[0-9]+$/.test(paneId) || seen.has(paneId) || (dead !== "0" && dead !== "1")
|
|
1991
|
+
|| !Number.isSafeInteger(panePid) || panePid <= 0 || !sessionName
|
|
1992
|
+
|| !/^\$[0-9]+$/.test(internalSessionId) || !/^[0-9]+$/.test(sessionCreated))
|
|
1993
|
+
return null;
|
|
1994
|
+
seen.add(paneId);
|
|
1995
|
+
if (paneId !== record.tmux_pane_id)
|
|
1996
|
+
continue;
|
|
1997
|
+
if (dead !== "0" || sessionName !== record.tmux_session_name || omxInstanceId !== record.session_id)
|
|
1998
|
+
return null;
|
|
1999
|
+
binding = { paneId, panePid, sessionName, internalSessionId, sessionCreated, omxInstanceId };
|
|
2000
|
+
}
|
|
2001
|
+
return binding;
|
|
2002
|
+
}
|
|
2003
|
+
catch {
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
function hasMatchingMadmaxDetachedRuntimeBinding(record) {
|
|
2008
|
+
const binding = queryMadmaxDetachedRuntimeBinding(record);
|
|
2009
|
+
return Boolean(binding
|
|
2010
|
+
&& binding.panePid === record.tmux_pane_pid
|
|
2011
|
+
&& binding.internalSessionId === record.tmux_internal_session_id
|
|
2012
|
+
&& binding.sessionCreated === record.tmux_session_created);
|
|
2013
|
+
}
|
|
1771
2014
|
function isReusableMadmaxDetachedActiveRecord(record) {
|
|
1772
|
-
if (!
|
|
2015
|
+
if (record.lifecycle_phase !== "ready" || !record.leader_pid || !record.base_state_root || !record.launch_nonce)
|
|
1773
2016
|
return false;
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2017
|
+
try {
|
|
2018
|
+
process.kill(record.leader_pid, 0);
|
|
2019
|
+
}
|
|
2020
|
+
catch {
|
|
1777
2021
|
return false;
|
|
1778
2022
|
}
|
|
1779
|
-
return tmuxPaneBelongsToSession(record.tmux_pane_id, record.tmux_session_name);
|
|
1780
|
-
}
|
|
1781
|
-
function detachedTmuxSessionExists(sessionName) {
|
|
1782
2023
|
try {
|
|
1783
|
-
|
|
1784
|
-
|
|
2024
|
+
if (realpathSync(record.base_state_root) !== record.base_state_root)
|
|
2025
|
+
return false;
|
|
1785
2026
|
}
|
|
1786
2027
|
catch {
|
|
1787
2028
|
return false;
|
|
1788
2029
|
}
|
|
2030
|
+
return hasMatchingMadmaxDetachedRuntimeBinding(record);
|
|
1789
2031
|
}
|
|
1790
2032
|
function readMadmaxDetachedLockOwner(lockPath) {
|
|
1791
2033
|
try {
|
|
@@ -1854,7 +2096,7 @@ function inspectMadmaxDetachedContextLock(lockPath) {
|
|
|
1854
2096
|
diagnostic: `lock has no readable owner pid yet; lock age ${Math.round(ageMs)}ms`,
|
|
1855
2097
|
};
|
|
1856
2098
|
}
|
|
1857
|
-
export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
|
|
2099
|
+
export async function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
|
|
1858
2100
|
const lockPath = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.lock`);
|
|
1859
2101
|
const maxAttempts = options.maxAttempts ?? MADMAX_DETACHED_LOCK_MAX_ATTEMPTS;
|
|
1860
2102
|
const retryMs = options.retryMs ?? MADMAX_DETACHED_LOCK_RETRY_MS;
|
|
@@ -1872,7 +2114,7 @@ export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options
|
|
|
1872
2114
|
};
|
|
1873
2115
|
writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`, { mode: 0o600 });
|
|
1874
2116
|
writeFileSync(join(lockPath, "pid"), String(process.pid));
|
|
1875
|
-
return run();
|
|
2117
|
+
return await run();
|
|
1876
2118
|
}
|
|
1877
2119
|
finally {
|
|
1878
2120
|
rmSync(lockPath, { recursive: true, force: true });
|
|
@@ -1900,6 +2142,7 @@ function readMadmaxRunMetadata(runRoot) {
|
|
|
1900
2142
|
const parsed = JSON.parse(readFileSync(join(runRoot, ".omxbox-run.json"), "utf-8"));
|
|
1901
2143
|
return {
|
|
1902
2144
|
...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
|
|
2145
|
+
...(typeof parsed.source_cwd === "string" ? { source_cwd: parsed.source_cwd } : {}),
|
|
1903
2146
|
...(typeof parsed.detached_launch_context === "string"
|
|
1904
2147
|
? { detached_launch_context: parsed.detached_launch_context }
|
|
1905
2148
|
: {}),
|
|
@@ -1927,13 +2170,27 @@ function madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env) {
|
|
|
1927
2170
|
return false;
|
|
1928
2171
|
if (metadata.cwd && metadata.cwd !== inheritedRoot)
|
|
1929
2172
|
return false;
|
|
2173
|
+
const sourceCwd = env.OMX_SOURCE_CWD?.trim();
|
|
2174
|
+
if (!sourceCwd || !metadata.source_cwd || metadata.source_cwd !== sourceCwd)
|
|
2175
|
+
return false;
|
|
1930
2176
|
if (metadata.detached_launch_context !== context)
|
|
1931
2177
|
return false;
|
|
1932
2178
|
const expectedContext = buildMadmaxDetachedLaunchContextKey(cwd, [...launchArgs], inheritedRoot);
|
|
1933
2179
|
return expectedContext === context;
|
|
1934
2180
|
}
|
|
1935
|
-
function
|
|
1936
|
-
|
|
2181
|
+
function resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, env) {
|
|
2182
|
+
if (!madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env))
|
|
2183
|
+
return undefined;
|
|
2184
|
+
const inheritedRoot = resolveInheritedMadmaxRoot(env);
|
|
2185
|
+
const context = env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
|
|
2186
|
+
if (!inheritedRoot || !context)
|
|
2187
|
+
return undefined;
|
|
2188
|
+
return {
|
|
2189
|
+
root: resolveLaunchPath(cwd, inheritedRoot),
|
|
2190
|
+
sourceCwd: env.OMX_SOURCE_CWD?.trim() || cwd,
|
|
2191
|
+
context,
|
|
2192
|
+
runsRoot: resolveMadmaxRunsRoot(env),
|
|
2193
|
+
};
|
|
1937
2194
|
}
|
|
1938
2195
|
function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
|
|
1939
2196
|
const runRoot = env.OMX_ROOT;
|
|
@@ -1945,18 +2202,243 @@ function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
|
|
|
1945
2202
|
rmSync(runRoot, { recursive: true, force: true });
|
|
1946
2203
|
}
|
|
1947
2204
|
function writeMadmaxDetachedActiveRecord(recordPath, record) {
|
|
1948
|
-
|
|
1949
|
-
|
|
2205
|
+
const bytes = `${JSON.stringify(record, null, 2)}\n`;
|
|
2206
|
+
const tempPath = `${recordPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
2207
|
+
let fd;
|
|
2208
|
+
mkdirSync(dirname(recordPath), { recursive: true, mode: 0o700 });
|
|
2209
|
+
try {
|
|
2210
|
+
fd = openSync(tempPath, "wx", 0o600);
|
|
2211
|
+
writeFileSync(fd, bytes, "utf-8");
|
|
2212
|
+
try {
|
|
2213
|
+
fsyncSync(fd);
|
|
2214
|
+
}
|
|
2215
|
+
catch (error) {
|
|
2216
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
2217
|
+
throw error;
|
|
2218
|
+
}
|
|
2219
|
+
closeSync(fd);
|
|
2220
|
+
fd = undefined;
|
|
2221
|
+
renameSync(tempPath, recordPath);
|
|
2222
|
+
const directoryFd = openSync(dirname(recordPath), "r");
|
|
2223
|
+
try {
|
|
2224
|
+
try {
|
|
2225
|
+
fsyncSync(directoryFd);
|
|
2226
|
+
}
|
|
2227
|
+
catch (error) {
|
|
2228
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
2229
|
+
throw error;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
finally {
|
|
2233
|
+
closeSync(directoryFd);
|
|
2234
|
+
}
|
|
2235
|
+
return {
|
|
2236
|
+
bytes,
|
|
2237
|
+
digest: createHash("sha256").update(bytes).digest("hex"),
|
|
2238
|
+
nonce: record.launch_nonce ?? "",
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
finally {
|
|
2242
|
+
if (fd !== undefined)
|
|
2243
|
+
closeSync(fd);
|
|
2244
|
+
try {
|
|
2245
|
+
unlinkSync(tempPath);
|
|
2246
|
+
}
|
|
2247
|
+
catch { }
|
|
2248
|
+
}
|
|
1950
2249
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
2250
|
+
function detachedFailureCode(error) {
|
|
2251
|
+
const code = error && typeof error === "object" && "code" in error
|
|
2252
|
+
? error.code
|
|
2253
|
+
: undefined;
|
|
2254
|
+
return typeof code === "string" && /^(?:EACCES|EAGAIN|EBUSY|ECANCELED|ECONNREFUSED|EEXIST|EINTR|EINVAL|EIO|EISDIR|EMFILE|ENFILE|ENOENT|ENOSPC|ENOTDIR|EPERM|EPIPE|EROFS|ESRCH|ETIMEDOUT)$/.test(code) ? code : "unknown";
|
|
2255
|
+
}
|
|
2256
|
+
export function describeDetachedLeaderFailure(error) {
|
|
2257
|
+
const describe = (value, depth) => {
|
|
2258
|
+
if (depth > 4)
|
|
2259
|
+
return "nested failure";
|
|
2260
|
+
if (value instanceof AggregateError) {
|
|
2261
|
+
return [value.message, ...[...value.errors].map((child) => describe(child, depth + 1))]
|
|
2262
|
+
.filter(Boolean)
|
|
2263
|
+
.join(": ");
|
|
2264
|
+
}
|
|
2265
|
+
if (value instanceof Error)
|
|
2266
|
+
return value.message;
|
|
2267
|
+
return String(value);
|
|
2268
|
+
};
|
|
2269
|
+
return describe(error, 0).replace(/[\r\n\t]+/g, " ").slice(0, 1_024);
|
|
2270
|
+
}
|
|
2271
|
+
export class DetachedLaunchSafetyError extends Error {
|
|
2272
|
+
phase;
|
|
2273
|
+
cause;
|
|
2274
|
+
report;
|
|
2275
|
+
name = "DetachedLaunchSafetyError";
|
|
2276
|
+
constructor(phase, cause, report) {
|
|
2277
|
+
super(`detached launch safety failure during ${phase}${cause instanceof Error ? `: ${describeDetachedLeaderFailure(cause)}` : ""}`);
|
|
2278
|
+
this.phase = phase;
|
|
2279
|
+
this.cause = cause;
|
|
2280
|
+
this.report = report;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
export class DetachedTransportUnavailable extends Error {
|
|
2284
|
+
reason;
|
|
2285
|
+
name = "DetachedTransportUnavailable";
|
|
2286
|
+
noSideEffects = true;
|
|
2287
|
+
constructor(reason) {
|
|
2288
|
+
super("detached transport is unavailable before launch side effects");
|
|
2289
|
+
this.reason = reason;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
2293
|
+
export async function executeDetachedLaunchStateMachine(input, deps) {
|
|
2294
|
+
const report = input.preflight.report;
|
|
2295
|
+
const transition = (name) => { report.transitions.push(name); };
|
|
2296
|
+
let binding;
|
|
2297
|
+
let ownedRecord;
|
|
2298
|
+
let released = false;
|
|
2299
|
+
let retainedAuthority = false;
|
|
2300
|
+
let rollbackAuthorized = false;
|
|
2301
|
+
try {
|
|
2302
|
+
transition("D1");
|
|
2303
|
+
binding = await deps.establish();
|
|
2304
|
+
retainedAuthority = true;
|
|
2305
|
+
transition("D2");
|
|
2306
|
+
const completion = await deps.complete(binding);
|
|
2307
|
+
if (completion.kind === "failure") {
|
|
2308
|
+
const failures = [completion.error];
|
|
2309
|
+
try {
|
|
2310
|
+
report.rollback.attempted.push("setup-finalization");
|
|
2311
|
+
await deps.finalizeSetupFailure(binding);
|
|
2312
|
+
}
|
|
2313
|
+
catch (finalizationError) {
|
|
2314
|
+
report.rollback.failures.push({ step: "setup-finalization", status: "failed", code: detachedFailureCode(finalizationError) });
|
|
2315
|
+
failures.push(finalizationError);
|
|
2316
|
+
}
|
|
2317
|
+
throw new DetachedLaunchSafetyError("completion", new AggregateError(failures, `preLaunch ${completion.operation} failed: ${describeDetachedLeaderFailure(completion.error)}`), report);
|
|
2318
|
+
}
|
|
2319
|
+
transition("D3");
|
|
2320
|
+
const inertSession = await deps.createInertSession();
|
|
2321
|
+
transition("D4");
|
|
2322
|
+
const pane = await deps.capturePane(inertSession);
|
|
2323
|
+
transition("D5");
|
|
2324
|
+
if (await deps.updateNameMetadata(binding, inertSession) !== "committed-released") {
|
|
2325
|
+
throw new DetachedLaunchSafetyError("name-metadata", new Error("detached name metadata did not release"), report);
|
|
2326
|
+
}
|
|
2327
|
+
transition("D6");
|
|
2328
|
+
if (await deps.updatePaneMetadata(binding, pane) !== "committed-released") {
|
|
2329
|
+
throw new DetachedLaunchSafetyError("pane-metadata", new Error("detached pane metadata did not release"), report);
|
|
2330
|
+
}
|
|
2331
|
+
transition("D7");
|
|
2332
|
+
ownedRecord = await deps.publishActiveRecord(inertSession, pane);
|
|
2333
|
+
transition("D9");
|
|
2334
|
+
try {
|
|
2335
|
+
await deps.releaseBarrier();
|
|
2336
|
+
released = true;
|
|
2337
|
+
}
|
|
2338
|
+
catch (releaseError) {
|
|
2339
|
+
// A failed publication must not let the outer launcher destroy the
|
|
2340
|
+
// leader's finalization capability. Request a nonce-bound abort and
|
|
2341
|
+
// preserve all artifacts on timeout, malformed, or foreign reports.
|
|
2342
|
+
let resolution;
|
|
2343
|
+
try {
|
|
2344
|
+
resolution = await deps.abortAndAwaitFinalization?.(releaseError);
|
|
2345
|
+
}
|
|
2346
|
+
catch (abortError) {
|
|
2347
|
+
released = true;
|
|
2348
|
+
throw new AggregateError([releaseError, abortError], "detached release and abort publication both failed");
|
|
2349
|
+
}
|
|
2350
|
+
rollbackAuthorized = resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
|
|
2351
|
+
Number.isSafeInteger(resolution.leaderPid) && resolution.kind);
|
|
2352
|
+
if (!rollbackAuthorized)
|
|
2353
|
+
released = true;
|
|
2354
|
+
throw releaseError;
|
|
2355
|
+
}
|
|
2356
|
+
transition("D10");
|
|
2357
|
+
await deps.attachOrReturn(inertSession, input.preflight.shouldAttach);
|
|
2358
|
+
return { kind: input.preflight.shouldAttach ? "attached" : "returned", released: true, report };
|
|
2359
|
+
}
|
|
2360
|
+
catch (error) {
|
|
2361
|
+
deps.diagnostic?.(error);
|
|
2362
|
+
if (retainedAuthority && !rollbackAuthorized && !released) {
|
|
2363
|
+
try {
|
|
2364
|
+
const resolution = await deps.abortAndAwaitFinalization?.(error);
|
|
2365
|
+
rollbackAuthorized = resolution?.rollbackAuthorized === true || (resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
|
|
2366
|
+
Number.isSafeInteger(resolution.leaderPid) && resolution.kind));
|
|
2367
|
+
if (!rollbackAuthorized)
|
|
2368
|
+
released = true;
|
|
2369
|
+
}
|
|
2370
|
+
catch (abortError) {
|
|
2371
|
+
released = true;
|
|
2372
|
+
deps.diagnostic?.(abortError);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (!released) {
|
|
2376
|
+
try {
|
|
2377
|
+
report.rollback.attempted.push("rollback");
|
|
2378
|
+
await deps.rollback(ownedRecord, report);
|
|
2379
|
+
}
|
|
2380
|
+
catch (rollbackError) {
|
|
2381
|
+
report.rollback.failures.push({ step: "rollback", status: "failed", code: detachedFailureCode(rollbackError) });
|
|
2382
|
+
deps.diagnostic?.(rollbackError);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (error instanceof DetachedLaunchSafetyError) {
|
|
2386
|
+
throw new DetachedLaunchSafetyError(error.phase, error.cause, report);
|
|
2387
|
+
}
|
|
2388
|
+
const phase = report.transitions.at(-1);
|
|
2389
|
+
const phaseName = phase === "D1" ? "establishment"
|
|
2390
|
+
: phase === "D2" ? "completion"
|
|
2391
|
+
: phase === "D3" ? "inert-session"
|
|
2392
|
+
: phase === "D4" ? "pane-id"
|
|
2393
|
+
: phase === "D5" ? "name-metadata"
|
|
2394
|
+
: phase === "D6" ? "pane-metadata"
|
|
2395
|
+
: phase === "D7" ? "active-record"
|
|
2396
|
+
: phase === "D9" ? "barrier-release"
|
|
2397
|
+
: "post-release-attach";
|
|
2398
|
+
throw new DetachedLaunchSafetyError(phaseName, error, report);
|
|
2399
|
+
}
|
|
1953
2400
|
}
|
|
1954
2401
|
class MadmaxDetachedGuardError extends Error {
|
|
1955
2402
|
failClosed = true;
|
|
1956
2403
|
}
|
|
2404
|
+
function preflightDetachedLaunch() {
|
|
2405
|
+
// D0 must remain transport-only: `tmux -V` neither connects to nor creates a server.
|
|
2406
|
+
probeDetachedTmuxTransport();
|
|
2407
|
+
const report = { transitions: ["D0"], rollback: { attempted: [], failures: [] } };
|
|
2408
|
+
return Object.freeze({
|
|
2409
|
+
kind: "available",
|
|
2410
|
+
shouldAttach: shouldAttachDetachedTmuxSession(process.env),
|
|
2411
|
+
report,
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
function handleMadmaxDetachedReuse(runtimeContext, preflight) {
|
|
2415
|
+
const contextKey = runtimeContext?.madmaxDetachedContext ?? process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
|
|
2416
|
+
const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey) : null;
|
|
2417
|
+
const activeRecord = activeRecordPath ? readMadmaxDetachedActiveRecord(activeRecordPath) : null;
|
|
2418
|
+
if (!activeRecord || activeRecord.context_key !== contextKey || !isReusableMadmaxDetachedActiveRecord(activeRecord))
|
|
2419
|
+
return false;
|
|
2420
|
+
cleanupCurrentMadmaxReuseRunRoot(process.env, resolveMadmaxRunsRoot(process.env));
|
|
2421
|
+
setDetachedTmuxSessionHistoryLimit(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
|
|
2422
|
+
if (!preflight.shouldAttach) {
|
|
2423
|
+
clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
|
|
2424
|
+
return true;
|
|
2425
|
+
}
|
|
2426
|
+
process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
|
|
2427
|
+
try {
|
|
2428
|
+
execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], { stdio: "inherit" });
|
|
2429
|
+
}
|
|
2430
|
+
catch (error) {
|
|
2431
|
+
throw new DetachedLaunchSafetyError("post-release-attach", error, preflight.report);
|
|
2432
|
+
}
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
1957
2435
|
export function createMadmaxIsolatedRoot(sourceCwd, argv, env = process.env) {
|
|
1958
|
-
|
|
2436
|
+
let runsRoot = resolveMadmaxRunsRoot(env);
|
|
1959
2437
|
mkdirSync(runsRoot, { recursive: true });
|
|
2438
|
+
// Re-resolve after creating it: on a first launch the directory does not exist yet, so the realpath
|
|
2439
|
+
// inside resolveMadmaxRunsRoot falls back to the raw value and the macOS /var vs /private/var alias
|
|
2440
|
+
// survives into the run directory and the active-record path.
|
|
2441
|
+
runsRoot = resolveMadmaxRunsRoot(env);
|
|
1960
2442
|
const stamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
|
1961
2443
|
const suffix = Math.random().toString(16).slice(2, 6);
|
|
1962
2444
|
const runDir = join(runsRoot, sanitizeRunIdSegment(`run-${stamp}-${suffix}`));
|
|
@@ -1984,6 +2466,24 @@ function activateMadmaxIsolationIfNeeded(command, launchArgs, cwd, env = process
|
|
|
1984
2466
|
env.OMX_SOURCE_CWD = cwd;
|
|
1985
2467
|
process.stderr.write(`[omx] madmax isolated state: ${runDir} (source: ${cwd})\n`);
|
|
1986
2468
|
}
|
|
2469
|
+
function renderDetachedLaunchSafetyReport(error) {
|
|
2470
|
+
const establishmentCleanup = error.report.establishmentCleanup?.capability.map((entry) => ({
|
|
2471
|
+
role: entry.role,
|
|
2472
|
+
phase: entry.phase,
|
|
2473
|
+
status: entry.status,
|
|
2474
|
+
...(entry.error ? { failureCode: detachedFailureCode(entry.error) } : {}),
|
|
2475
|
+
}));
|
|
2476
|
+
return JSON.stringify({
|
|
2477
|
+
phase: error.phase,
|
|
2478
|
+
failure: describeDetachedLeaderFailure(error.cause),
|
|
2479
|
+
transitions: error.report.transitions,
|
|
2480
|
+
...(establishmentCleanup ? { establishmentCleanup } : {}),
|
|
2481
|
+
rollback: {
|
|
2482
|
+
attempted: error.report.rollback.attempted,
|
|
2483
|
+
failures: error.report.rollback.failures.map(({ step, status, code }) => ({ step, status, code })),
|
|
2484
|
+
},
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
1987
2487
|
export async function main(args) {
|
|
1988
2488
|
const knownCommands = new Set([
|
|
1989
2489
|
"launch",
|
|
@@ -1992,6 +2492,7 @@ export async function main(args) {
|
|
|
1992
2492
|
"imagegen",
|
|
1993
2493
|
"capabilities",
|
|
1994
2494
|
"setup",
|
|
2495
|
+
"__detached-session-leader",
|
|
1995
2496
|
"update",
|
|
1996
2497
|
"list",
|
|
1997
2498
|
"agents",
|
|
@@ -2009,6 +2510,7 @@ export async function main(args) {
|
|
|
2009
2510
|
"api",
|
|
2010
2511
|
"sparkshell",
|
|
2011
2512
|
"team",
|
|
2513
|
+
"autopilot",
|
|
2012
2514
|
"ralph",
|
|
2013
2515
|
"ralplan",
|
|
2014
2516
|
"ultragoal",
|
|
@@ -2038,11 +2540,15 @@ export async function main(args) {
|
|
|
2038
2540
|
dryRun: flags.has("--dry-run"),
|
|
2039
2541
|
verbose: flags.has("--verbose"),
|
|
2040
2542
|
team: flags.has("--team"),
|
|
2543
|
+
repairState: flags.has("--repair-state"),
|
|
2041
2544
|
};
|
|
2042
2545
|
if (flags.has("--help") && !commandOwnsLocalHelp(command)) {
|
|
2043
2546
|
console.log(HELP);
|
|
2044
2547
|
return;
|
|
2045
2548
|
}
|
|
2549
|
+
if (command !== "launch" && command !== "resume") {
|
|
2550
|
+
activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
|
|
2551
|
+
}
|
|
2046
2552
|
try {
|
|
2047
2553
|
if (command === "session") {
|
|
2048
2554
|
await sessionCommand(args.slice(1));
|
|
@@ -2052,8 +2558,13 @@ export async function main(args) {
|
|
|
2052
2558
|
console.log(RESUME_HELP);
|
|
2053
2559
|
return;
|
|
2054
2560
|
}
|
|
2055
|
-
activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
|
|
2056
2561
|
switch (command) {
|
|
2562
|
+
case "__detached-session-leader": {
|
|
2563
|
+
const payload = decodeDetachedLeaderPayload(launchArgs[0]);
|
|
2564
|
+
await runDetachedSessionLeader(payload);
|
|
2565
|
+
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
2566
|
+
break;
|
|
2567
|
+
}
|
|
2057
2568
|
case "launch":
|
|
2058
2569
|
if (launchArgsRequestAuthHotswap(launchArgs)) {
|
|
2059
2570
|
await launchWithAuthHotswap(launchArgs);
|
|
@@ -2067,6 +2578,7 @@ export async function main(args) {
|
|
|
2067
2578
|
break;
|
|
2068
2579
|
case "setup":
|
|
2069
2580
|
await setup({
|
|
2581
|
+
disableHooks: flags.has("--disable-hooks"),
|
|
2070
2582
|
force: options.force,
|
|
2071
2583
|
mergeAgents: options.mergeAgents,
|
|
2072
2584
|
mergeAgentsPolicy: resolveSetupAgentsMergePolicyArg(args.slice(1)),
|
|
@@ -2166,6 +2678,9 @@ export async function main(args) {
|
|
|
2166
2678
|
case "team":
|
|
2167
2679
|
await teamCommand(args.slice(1), options);
|
|
2168
2680
|
break;
|
|
2681
|
+
case "autopilot":
|
|
2682
|
+
await autopilotCommand(args.slice(1));
|
|
2683
|
+
break;
|
|
2169
2684
|
case "url":
|
|
2170
2685
|
await urlCommand(args.slice(1));
|
|
2171
2686
|
break;
|
|
@@ -2249,7 +2764,13 @@ export async function main(args) {
|
|
|
2249
2764
|
}
|
|
2250
2765
|
}
|
|
2251
2766
|
catch (err) {
|
|
2252
|
-
|
|
2767
|
+
if (err instanceof DetachedLaunchSafetyError) {
|
|
2768
|
+
console.error(`Error: ${err.message}`);
|
|
2769
|
+
console.error(`[omx] detached launch safety report: ${renderDetachedLaunchSafetyReport(err)}`);
|
|
2770
|
+
}
|
|
2771
|
+
else {
|
|
2772
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
2773
|
+
}
|
|
2253
2774
|
process.exit(1);
|
|
2254
2775
|
}
|
|
2255
2776
|
}
|
|
@@ -2480,38 +3001,29 @@ export async function launchWithAuthHotswap(args) {
|
|
|
2480
3001
|
process.exitCode = status;
|
|
2481
3002
|
}
|
|
2482
3003
|
export async function launchWithHud(args) {
|
|
2483
|
-
if (isNativeWindows()) {
|
|
2484
|
-
const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
|
|
2485
|
-
encoding: "utf-8",
|
|
2486
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2487
|
-
});
|
|
2488
|
-
if (result.error) {
|
|
2489
|
-
const errno = result.error;
|
|
2490
|
-
const kind = classifySpawnError(errno);
|
|
2491
|
-
if (kind === "missing") {
|
|
2492
|
-
console.warn("[omx] warning: tmux was not found on native Windows. Continuing without tmux/HUD.\n" +
|
|
2493
|
-
"[omx] To enable tmux-backed features, install psmux:\n" +
|
|
2494
|
-
"[omx] winget install psmux\n" +
|
|
2495
|
-
"[omx] See: https://github.com/marlocarlo/psmux");
|
|
2496
|
-
}
|
|
2497
|
-
else {
|
|
2498
|
-
console.warn(`[omx] warning: tmux probe failed on native Windows (${errno.code || errno.message}). Continuing without tmux/HUD.`);
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
else if (result.status !== 0 && !isTmuxAvailable()) {
|
|
2502
|
-
const stderr = (result.stderr || "").trim();
|
|
2503
|
-
console.warn(`[omx] warning: tmux reported an error on native Windows${stderr ? ` (${stderr})` : ""}. Continuing without tmux/HUD.`);
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
3004
|
const launchCwd = process.cwd();
|
|
2507
3005
|
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
2508
3006
|
const parsedWorktree = parseWorktreeMode(omxArgs);
|
|
2509
3007
|
const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
|
|
2510
3008
|
const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
|
|
2511
3009
|
const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(passthroughArgs, process.env);
|
|
2512
|
-
|
|
2513
|
-
|
|
3010
|
+
let { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
|
|
3011
|
+
let detachedPreflight;
|
|
3012
|
+
if (launchPolicy === "detached-tmux") {
|
|
3013
|
+
try {
|
|
3014
|
+
detachedPreflight = preflightDetachedLaunch();
|
|
3015
|
+
}
|
|
3016
|
+
catch (error) {
|
|
3017
|
+
if (!(error instanceof DetachedTransportUnavailable))
|
|
3018
|
+
throw error;
|
|
3019
|
+
console.warn("[omx] warning: detached tmux transport is unavailable. Falling back to direct Codex launch.");
|
|
3020
|
+
launchPolicy = "direct";
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
2514
3023
|
const enableNotifyFallbackAuthority = launchPolicy === "direct";
|
|
3024
|
+
activateMadmaxIsolationIfNeeded("launch", args, launchCwd, process.env);
|
|
3025
|
+
const verifiedMadmaxLaunchContext = resolveVerifiedMadmaxDetachedContext(launchCwd, args, process.env);
|
|
3026
|
+
const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
|
|
2515
3027
|
const workerSparkModel = resolveWorkerSparkModel(passthroughArgs, persistentCodexHomeForLaunch);
|
|
2516
3028
|
let normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
|
|
2517
3029
|
let cwd = launchCwd;
|
|
@@ -2551,6 +3063,8 @@ export async function launchWithHud(args) {
|
|
|
2551
3063
|
clearInheritedMadmaxRootForDisposableWorktreeLaunch(parsedWorktree.remainingArgs);
|
|
2552
3064
|
applyDisposableWorktreeOmxRootForLaunch(ensuredLaunchWorktree);
|
|
2553
3065
|
applyWorktreeToolContextForLaunch(cwd, ensuredLaunchWorktree);
|
|
3066
|
+
if (launchPolicy === "detached-tmux" && detachedPreflight && handleMadmaxDetachedReuse(madmaxWorktreeRuntimeContext, detachedPreflight))
|
|
3067
|
+
return;
|
|
2554
3068
|
const sessionId = `omx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2555
3069
|
try {
|
|
2556
3070
|
await maybeCheckAndPromptUpdate(cwd);
|
|
@@ -2592,37 +3106,77 @@ export async function launchWithHud(args) {
|
|
|
2592
3106
|
const codexHomeOverride = preparedCodexHome.codexHomeOverride;
|
|
2593
3107
|
const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
|
|
2594
3108
|
const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
3109
|
+
let insideTmuxControlPlane;
|
|
3110
|
+
if (launchPolicy === "inside-tmux") {
|
|
3111
|
+
let verifiedTeamContext;
|
|
3112
|
+
try {
|
|
3113
|
+
verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
|
|
3114
|
+
}
|
|
3115
|
+
catch {
|
|
3116
|
+
verifiedTeamContext = undefined;
|
|
3117
|
+
}
|
|
3118
|
+
insideTmuxControlPlane = buildDetachedLaunchControlPlane({
|
|
3119
|
+
cwd,
|
|
3120
|
+
sessionId,
|
|
3121
|
+
env: process.env,
|
|
3122
|
+
omxRootOverride: resolveOmxRootForLaunch(cwd, process.env),
|
|
3123
|
+
runtimeContext: madmaxWorktreeRuntimeContext,
|
|
3124
|
+
verifiedMadmaxContext: verifiedMadmaxLaunchContext,
|
|
3125
|
+
leaderPaneId: process.env.TMUX_PANE,
|
|
3126
|
+
verifiedTeamContext,
|
|
3127
|
+
});
|
|
2598
3128
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
3129
|
+
const restoreInsideTmuxControlPlane = insideTmuxControlPlane
|
|
3130
|
+
? applyLaunchOwnedControlPlane(insideTmuxControlPlane)
|
|
3131
|
+
: undefined;
|
|
3132
|
+
let launch;
|
|
3133
|
+
if (launchPolicy !== "detached-tmux") {
|
|
3134
|
+
try {
|
|
3135
|
+
launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty);
|
|
3136
|
+
}
|
|
3137
|
+
catch (err) {
|
|
3138
|
+
logEstablishmentCleanupEvidence(err);
|
|
3139
|
+
if (isSessionPointerLaunchAbort(err)) {
|
|
3140
|
+
console.error(`[omx] session pointer launch aborted: ${err.code}`);
|
|
3141
|
+
reportOrdinaryLaunchRootConflict(err, cwd, cwd === launchCwd && !parsedWorktree.mode.enabled && !isResumeCodexLaunch(normalizedArgs));
|
|
3142
|
+
}
|
|
3143
|
+
restoreInsideTmuxControlPlane?.();
|
|
2603
3144
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
2604
3145
|
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
2605
3146
|
});
|
|
2606
3147
|
throw err;
|
|
2607
3148
|
}
|
|
2608
|
-
// preLaunch errors after pointer commit must not prevent Codex from starting.
|
|
2609
|
-
console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
|
|
2610
3149
|
}
|
|
2611
|
-
// ── Phase 2: run ────────────────────────────────────────────────────────
|
|
2612
3150
|
let postLaunchHandledExternally = false;
|
|
2613
3151
|
try {
|
|
2614
3152
|
const notifyTempContractRaw = notifyTempResult.contract.active
|
|
2615
3153
|
? serializeNotifyTempContract(notifyTempResult.contract)
|
|
2616
3154
|
: null;
|
|
2617
|
-
const launchResult = runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw,
|
|
3155
|
+
const launchResult = await runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, {
|
|
3156
|
+
notifyTempContract: notifyTempResult.contract,
|
|
3157
|
+
enableNotifyFallbackAuthority,
|
|
3158
|
+
worktreeDirty,
|
|
3159
|
+
}, launchPolicy, detachedPreflight, projectLocalCodexHomeForCleanup, preparedCodexHome.runtimeCodexHomeForCleanup, madmaxWorktreeRuntimeContext, verifiedMadmaxLaunchContext, insideTmuxControlPlane);
|
|
2618
3160
|
postLaunchHandledExternally = launchResult.postLaunchHandledExternally;
|
|
2619
3161
|
}
|
|
3162
|
+
catch (error) {
|
|
3163
|
+
if (error instanceof DetachedLaunchSafetyError)
|
|
3164
|
+
postLaunchHandledExternally = true;
|
|
3165
|
+
throw error;
|
|
3166
|
+
}
|
|
2620
3167
|
finally {
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
3168
|
+
if (!postLaunchHandledExternally && launch) {
|
|
3169
|
+
try {
|
|
3170
|
+
await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
|
|
3171
|
+
}
|
|
3172
|
+
finally {
|
|
3173
|
+
restoreInsideTmuxControlPlane?.();
|
|
3174
|
+
}
|
|
2624
3175
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
|
|
2625
3176
|
}
|
|
3177
|
+
else {
|
|
3178
|
+
restoreInsideTmuxControlPlane?.();
|
|
3179
|
+
}
|
|
2626
3180
|
}
|
|
2627
3181
|
}
|
|
2628
3182
|
export async function execWithOverlay(args) {
|
|
@@ -2689,18 +3243,19 @@ export async function execWithOverlay(args) {
|
|
|
2689
3243
|
const codexHomeOverride = preparedCodexHome.codexHomeOverride;
|
|
2690
3244
|
const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
|
|
2691
3245
|
const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
|
|
3246
|
+
let launch;
|
|
2692
3247
|
try {
|
|
2693
|
-
await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
|
|
3248
|
+
launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
|
|
2694
3249
|
}
|
|
2695
3250
|
catch (err) {
|
|
3251
|
+
logEstablishmentCleanupEvidence(err);
|
|
2696
3252
|
if (isSessionPointerLaunchAbort(err)) {
|
|
2697
3253
|
console.error(`[omx] session pointer launch aborted: ${err.code}`);
|
|
2698
|
-
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
2699
|
-
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
2700
|
-
});
|
|
2701
|
-
throw err;
|
|
2702
3254
|
}
|
|
2703
|
-
|
|
3255
|
+
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
3256
|
+
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
3257
|
+
});
|
|
3258
|
+
throw err;
|
|
2704
3259
|
}
|
|
2705
3260
|
try {
|
|
2706
3261
|
const notifyTempContractRaw = notifyTempResult.contract.active
|
|
@@ -2708,22 +3263,28 @@ export async function execWithOverlay(args) {
|
|
|
2708
3263
|
: null;
|
|
2709
3264
|
const codexArgs = injectModelInstructionsBypassArgs(cwd, ["exec", ...normalizedArgs], process.env, sessionModelInstructionsPath(cwd, sessionId));
|
|
2710
3265
|
const omxRootOverride = resolveOmxRootForLaunch(cwd, process.env);
|
|
2711
|
-
const
|
|
2712
|
-
|
|
3266
|
+
const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
|
|
3267
|
+
if (!omxBin)
|
|
3268
|
+
throw new Error("Unable to resolve OMX launcher path for exec");
|
|
3269
|
+
const hudRuntimeRoot = resolveHudRuntimeRootForLaunch(cwd, process.env);
|
|
3270
|
+
const codexEnvBase = prependOmxRuntimeCommandShimToEnv(cwd, {
|
|
3271
|
+
...stripHermesMcpBridgeEnv(process.env),
|
|
2713
3272
|
...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
|
|
2714
3273
|
...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
|
|
2715
3274
|
...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
3275
|
+
}, omxBin);
|
|
3276
|
+
// Correlates plugin hook routing only; it is not an authority token.
|
|
3277
|
+
const pluginHookRoutingLaunchId = randomUUID();
|
|
3278
|
+
const codexEnv = {
|
|
3279
|
+
...codexEnvBase,
|
|
3280
|
+
OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
|
|
3281
|
+
...buildHudRuntimeEnv({ sessionId, ...hudRuntimeRoot }).env,
|
|
3282
|
+
...(notifyTempContractRaw ? { [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw } : {}),
|
|
2716
3283
|
};
|
|
2717
|
-
const codexEnv = notifyTempContractRaw
|
|
2718
|
-
? {
|
|
2719
|
-
...codexEnvBase,
|
|
2720
|
-
[OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw,
|
|
2721
|
-
}
|
|
2722
|
-
: codexEnvBase;
|
|
2723
3284
|
runCodexBlocking(cwd, codexArgs, codexEnv);
|
|
2724
3285
|
}
|
|
2725
3286
|
finally {
|
|
2726
|
-
await postLaunch(cwd, sessionId, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
|
|
3287
|
+
await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
|
|
2727
3288
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
|
|
2728
3289
|
}
|
|
2729
3290
|
}
|
|
@@ -3005,9 +3566,6 @@ export function detectDetachedSessionWindowIndex(sessionName) {
|
|
|
3005
3566
|
return null;
|
|
3006
3567
|
}
|
|
3007
3568
|
}
|
|
3008
|
-
function escapeShellDoubleQuotedValue(value) {
|
|
3009
|
-
return value.replace(/["\\$`]/g, "\\$&");
|
|
3010
|
-
}
|
|
3011
3569
|
function sanitizeTmuxLeaseKey(value) {
|
|
3012
3570
|
const cleaned = value
|
|
3013
3571
|
.toLowerCase()
|
|
@@ -3193,106 +3751,173 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
|
|
|
3193
3751
|
}
|
|
3194
3752
|
throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
|
|
3195
3753
|
}
|
|
3196
|
-
function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
"fi;",
|
|
3224
|
-
"exit $status;",
|
|
3225
|
-
"};",
|
|
3226
|
-
"trap omx_detached_session_cleanup 0 INT TERM HUP;",
|
|
3227
|
-
parentEnvSource,
|
|
3228
|
-
"unset OMX_HERMES_MCP_BRIDGE;",
|
|
3229
|
-
"omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
|
|
3230
|
-
`${codexCmd} <&3 &`,
|
|
3231
|
-
"omx_codex_pid=$!;",
|
|
3232
|
-
'wait "$omx_codex_pid";',
|
|
3233
|
-
"omx_codex_status=$?;",
|
|
3234
|
-
"omx_codex_finished_at=$(date +%s 2>/dev/null || printf 0);",
|
|
3235
|
-
'omx_codex_elapsed=$((omx_codex_finished_at - omx_codex_started_at));',
|
|
3236
|
-
'if [ "$omx_codex_status" -eq 0 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
|
|
3237
|
-
'printf "\\n[omx] codex exited immediately with code 0 during startup. The detached tmux session is being kept open so any output above remains visible. Press Enter to close this OMX session.\\n" >&2;',
|
|
3238
|
-
'IFS= read -r _omx_close || true;',
|
|
3239
|
-
'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
|
|
3240
|
-
'printf "\\n[omx] codex exited with code %s during startup. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
|
|
3241
|
-
'IFS= read -r _omx_close || true;',
|
|
3242
|
-
'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ]; then',
|
|
3243
|
-
'printf "\\n[omx] codex exited with code %s. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
|
|
3244
|
-
'IFS= read -r _omx_close || true;',
|
|
3245
|
-
"fi;",
|
|
3246
|
-
'exit "$omx_codex_status";',
|
|
3247
|
-
].join(" ");
|
|
3248
|
-
return `/bin/sh -c ${quoteShellArg(wrapped)}`;
|
|
3249
|
-
}
|
|
3250
|
-
function buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
|
|
3251
|
-
const cwdLiteral = JSON.stringify(cwd);
|
|
3252
|
-
const sessionIdLiteral = JSON.stringify(sessionId);
|
|
3253
|
-
const codexHomeLiteral = typeof codexHomeOverride === "string" && codexHomeOverride.length > 0
|
|
3254
|
-
? JSON.stringify(codexHomeOverride)
|
|
3255
|
-
: "undefined";
|
|
3256
|
-
const projectLocalCleanupLiteral = typeof projectLocalCodexHomeForCleanup === "string" &&
|
|
3257
|
-
projectLocalCodexHomeForCleanup.length > 0
|
|
3258
|
-
? JSON.stringify(projectLocalCodexHomeForCleanup)
|
|
3259
|
-
: "undefined";
|
|
3260
|
-
const runtimeCodexHomeCleanupLiteral = typeof runtimeCodexHomeForCleanup === "string" &&
|
|
3261
|
-
runtimeCodexHomeForCleanup.length > 0
|
|
3262
|
-
? JSON.stringify(runtimeCodexHomeForCleanup)
|
|
3263
|
-
: "undefined";
|
|
3264
|
-
const moduleUrlLiteral = JSON.stringify(import.meta.url);
|
|
3265
|
-
const script = [
|
|
3266
|
-
`const mod = await import(${moduleUrlLiteral});`,
|
|
3267
|
-
`await mod.runDetachedSessionPostLaunch(${cwdLiteral}, ${sessionIdLiteral}, ${codexHomeLiteral}, ${projectLocalCleanupLiteral}, ${runtimeCodexHomeCleanupLiteral});`,
|
|
3268
|
-
].join(" ");
|
|
3269
|
-
return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
|
|
3270
|
-
}
|
|
3271
|
-
function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
|
|
3272
|
-
encoding: "utf-8",
|
|
3273
|
-
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
3274
|
-
})) {
|
|
3275
|
-
return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
|
|
3754
|
+
function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
|
|
3755
|
+
const payload = Buffer.from(JSON.stringify({
|
|
3756
|
+
cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
|
|
3757
|
+
projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath,
|
|
3758
|
+
readyPath, preLaunchOptions,
|
|
3759
|
+
})).toString("base64url");
|
|
3760
|
+
return `/bin/sh -c ${quoteShellArg(`${parentEnvFilePath ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi; ` : ""}exec ${quoteShellArg(process.execPath)} ${quoteShellArg(omxBin)} __detached-session-leader ${quoteShellArg(payload)}`)}`;
|
|
3761
|
+
}
|
|
3762
|
+
function buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnv, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
|
|
3763
|
+
const payload = Buffer.from(JSON.stringify({
|
|
3764
|
+
cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
|
|
3765
|
+
projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup,
|
|
3766
|
+
...(parentEnv ? { parentEnv: detachedSessionParentEnvironment(parentEnv, true) } : {}),
|
|
3767
|
+
readyPath, preLaunchOptions,
|
|
3768
|
+
})).toString("base64url");
|
|
3769
|
+
const invocation = `& ${quotePowerShellArg(process.execPath)} ${quotePowerShellArg(omxBin)} __detached-session-leader ${quotePowerShellArg(payload)}`;
|
|
3770
|
+
return `powershell.exe -NoLogo -NoProfile -NonInteractive -Command ${quotePowerShellArg(invocation)}`;
|
|
3771
|
+
}
|
|
3772
|
+
export function isDetachedReadyReportAuthorized(report, expected) {
|
|
3773
|
+
if (report?.kind !== "ready" || report.nonce !== expected.nonce
|
|
3774
|
+
|| report.sessionId !== expected.sessionId || report.sessionName !== expected.sessionName
|
|
3775
|
+
|| typeof report.leaderPid !== "number" || report.leaderPid <= 0)
|
|
3776
|
+
return false;
|
|
3777
|
+
if (expected.shouldAttach && expected.leaderPanePid !== null) {
|
|
3778
|
+
return report.leaderPid === expected.leaderPanePid;
|
|
3779
|
+
}
|
|
3780
|
+
return report.paneId === expected.leaderPaneId;
|
|
3276
3781
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3782
|
+
function writeDetachedLeaderReport(path, report) {
|
|
3783
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
3784
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3785
|
+
let fd;
|
|
3281
3786
|
try {
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3787
|
+
fd = openSync(tempPath, "wx", 0o600);
|
|
3788
|
+
writeFileSync(fd, `${JSON.stringify(report)}\n`, "utf-8");
|
|
3789
|
+
try {
|
|
3790
|
+
fsyncSync(fd);
|
|
3791
|
+
}
|
|
3792
|
+
catch (error) {
|
|
3793
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
3794
|
+
throw error;
|
|
3795
|
+
}
|
|
3796
|
+
closeSync(fd);
|
|
3797
|
+
fd = undefined;
|
|
3798
|
+
renameSync(tempPath, path);
|
|
3799
|
+
const directoryFd = openSync(dirname(path), "r");
|
|
3800
|
+
try {
|
|
3801
|
+
try {
|
|
3802
|
+
fsyncSync(directoryFd);
|
|
3292
3803
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3804
|
+
catch (error) {
|
|
3805
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
3806
|
+
throw error;
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
finally {
|
|
3810
|
+
closeSync(directoryFd);
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
finally {
|
|
3814
|
+
if (fd !== undefined)
|
|
3815
|
+
closeSync(fd);
|
|
3816
|
+
try {
|
|
3817
|
+
unlinkSync(tempPath);
|
|
3818
|
+
}
|
|
3819
|
+
catch { }
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
function readDetachedLeaderReport(path) {
|
|
3823
|
+
try {
|
|
3824
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
3825
|
+
if (!value || typeof value !== "object")
|
|
3826
|
+
return undefined;
|
|
3827
|
+
const report = value;
|
|
3828
|
+
if (report.version !== 1 || !["ready", "failed", "terminal", "release", "abort"].includes(String(report.kind)) ||
|
|
3829
|
+
typeof report.nonce !== "string" || typeof report.sessionId !== "string" || typeof report.sessionName !== "string")
|
|
3830
|
+
return undefined;
|
|
3831
|
+
if (report.paneId !== undefined && typeof report.paneId !== "string")
|
|
3832
|
+
return undefined;
|
|
3833
|
+
if (report.leaderPid !== undefined && (!Number.isSafeInteger(report.leaderPid) || Number(report.leaderPid) <= 0))
|
|
3834
|
+
return undefined;
|
|
3835
|
+
if (report.exitStatus !== undefined && (!Number.isSafeInteger(report.exitStatus) || Number(report.exitStatus) < 0 || Number(report.exitStatus) > 255))
|
|
3836
|
+
return undefined;
|
|
3837
|
+
return report;
|
|
3838
|
+
}
|
|
3839
|
+
catch {
|
|
3840
|
+
return undefined;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3844
|
+
export function publishDetachedReleaseMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid, hud) {
|
|
3845
|
+
writeDetachedLeaderReport(`${releaseMarkerPath}.release`, {
|
|
3846
|
+
version: 1, kind: "release", nonce, sessionId, sessionName, leaderPid, ...(hud ? { hud } : {}),
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3850
|
+
export function applyDetachedTerminalExitStatus(releaseMarkerPath, expected) {
|
|
3851
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
3852
|
+
if (!report || report.kind !== "terminal" || report.finalized !== true)
|
|
3853
|
+
return false;
|
|
3854
|
+
if (report.nonce !== expected.nonce || report.sessionId !== expected.sessionId ||
|
|
3855
|
+
report.sessionName !== expected.sessionName || report.leaderPid !== expected.leaderPid)
|
|
3856
|
+
return false;
|
|
3857
|
+
if (typeof report.exitStatus !== "number")
|
|
3858
|
+
return false;
|
|
3859
|
+
process.exitCode = report.exitStatus;
|
|
3860
|
+
return true;
|
|
3861
|
+
}
|
|
3862
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3863
|
+
export function probeExactDetachedSessionExists(authority) {
|
|
3864
|
+
if (!authority)
|
|
3865
|
+
return undefined;
|
|
3866
|
+
try {
|
|
3867
|
+
// A retained dead pane (remain-on-exit) keeps its pid/topology, so prove
|
|
3868
|
+
// liveness first: a dead leader with no valid terminal report is a failure,
|
|
3869
|
+
// never a manual detach.
|
|
3870
|
+
const paneDead = execTmuxFileSync(["display-message", "-p", "-t", authority.paneId, "#{pane_dead}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3871
|
+
if (paneDead === "1")
|
|
3872
|
+
return false;
|
|
3873
|
+
if (paneDead !== "0")
|
|
3874
|
+
return undefined;
|
|
3875
|
+
// Re-capture the exact leader-pane topology the bootstrap authority recorded.
|
|
3876
|
+
// A matching recapture proves the owned session and its leader survive (manual
|
|
3877
|
+
// detach); a destroyed session, recycled pane id, or tmux error fails closed.
|
|
3878
|
+
const current = captureDetachedLeaderAuthority(authority.paneId, authority.sessionName, authority.ownerId);
|
|
3879
|
+
return current.panePid === authority.panePid && current.sessionId === authority.sessionId
|
|
3880
|
+
&& current.sessionCreated === authority.sessionCreated && current.windowId === authority.windowId
|
|
3881
|
+
&& current.windowIndex === authority.windowIndex;
|
|
3882
|
+
}
|
|
3883
|
+
catch {
|
|
3884
|
+
return undefined;
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3888
|
+
export function resolveDetachedAttachExitStatus(releaseMarkerPath, expected, probe) {
|
|
3889
|
+
if (applyDetachedTerminalExitStatus(releaseMarkerPath, expected))
|
|
3890
|
+
return "applied";
|
|
3891
|
+
return probe.exactSessionExists() === true ? "manual-detach" : "protocol-failure";
|
|
3892
|
+
}
|
|
3893
|
+
function publishDetachedAbortMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid) {
|
|
3894
|
+
writeDetachedLeaderReport(`${releaseMarkerPath}.abort`, { version: 1, kind: "abort", nonce, sessionId, sessionName, leaderPid });
|
|
3895
|
+
}
|
|
3896
|
+
function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
|
|
3897
|
+
encoding: "utf-8",
|
|
3898
|
+
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
3899
|
+
})) {
|
|
3900
|
+
return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
|
|
3901
|
+
}
|
|
3902
|
+
export function acquireTmuxExtendedKeysLease(cwd, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
|
|
3903
|
+
encoding: "utf-8",
|
|
3904
|
+
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
3905
|
+
}), ownerPid = process.pid) {
|
|
3906
|
+
try {
|
|
3907
|
+
const socketPath = resolveTmuxSocketPath(execFileSyncImpl);
|
|
3908
|
+
const leasePath = tmuxExtendedKeysLeasePath(cwd, socketPath);
|
|
3909
|
+
const holderPid = Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : process.pid;
|
|
3910
|
+
const leaseId = `${holderPid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
3911
|
+
withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
|
|
3912
|
+
const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
|
|
3913
|
+
const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
|
|
3914
|
+
if (stateRaw && state?.holders.length === 0) {
|
|
3915
|
+
execTmuxSync(["set-option", "-sq", "extended-keys", state.originalMode], execFileSyncImpl);
|
|
3916
|
+
rmSync(leasePath, { force: true });
|
|
3917
|
+
}
|
|
3918
|
+
if (!state || state.holders.length === 0) {
|
|
3919
|
+
const previousMode = execTmuxSync(["show-options", "-sv", "extended-keys"], execFileSyncImpl) ||
|
|
3920
|
+
TMUX_EXTENDED_KEYS_FALLBACK_MODE;
|
|
3296
3921
|
execTmuxSync(["set-option", "-sq", "extended-keys", TMUX_EXTENDED_KEYS_MODE], execFileSyncImpl);
|
|
3297
3922
|
writeTmuxExtendedKeysLeaseState(leasePath, {
|
|
3298
3923
|
originalMode: previousMode,
|
|
@@ -3352,19 +3977,123 @@ export function releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl
|
|
|
3352
3977
|
}
|
|
3353
3978
|
}
|
|
3354
3979
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3980
|
+
export const DETACHED_LAUNCH_CONTROL_PLANE_KEYS = [
|
|
3981
|
+
"OMX_SESSION_ID",
|
|
3982
|
+
"CODEX_SESSION_ID",
|
|
3983
|
+
"SESSION_ID",
|
|
3984
|
+
"OMX_TMUX_HUD_OWNER",
|
|
3985
|
+
"OMX_TMUX_HUD_LEADER_PANE",
|
|
3986
|
+
"OMX_ROOT",
|
|
3987
|
+
"OMX_STATE_ROOT",
|
|
3988
|
+
"OMX_TEAM_STATE_ROOT",
|
|
3989
|
+
"OMXBOX_ACTIVE",
|
|
3990
|
+
"OMX_SOURCE_CWD",
|
|
3991
|
+
"OMX_MADMAX_DETACHED_CONTEXT",
|
|
3992
|
+
"OMX_RUNS_DIR",
|
|
3993
|
+
"OMX_TEAM_LEADER_CWD",
|
|
3994
|
+
"OMX_TEAM_WORKER",
|
|
3995
|
+
"OMX_TEAM_INTERNAL_WORKER",
|
|
3996
|
+
];
|
|
3997
|
+
const DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET = new Set(DETACHED_LAUNCH_CONTROL_PLANE_KEYS);
|
|
3998
|
+
function isDetachedLaunchControlPlaneKey(key, caseInsensitive = false) {
|
|
3999
|
+
return caseInsensitive
|
|
4000
|
+
? DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key.toUpperCase())
|
|
4001
|
+
: DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key);
|
|
4002
|
+
}
|
|
4003
|
+
function emptyDetachedLaunchControlPlaneValues() {
|
|
4004
|
+
return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, ""]));
|
|
4005
|
+
}
|
|
4006
|
+
function resolveDetachedRootValue(cwd, raw) {
|
|
4007
|
+
return isCrossPlatformAbsolutePath(raw) ? raw : resolve(cwd, raw);
|
|
4008
|
+
}
|
|
4009
|
+
export function buildDetachedLaunchControlPlane(options) {
|
|
4010
|
+
const env = options.env ?? process.env;
|
|
4011
|
+
const values = emptyDetachedLaunchControlPlaneValues();
|
|
4012
|
+
values.OMX_SESSION_ID = options.sessionId;
|
|
4013
|
+
values.OMX_TMUX_HUD_OWNER = "1";
|
|
4014
|
+
values.OMX_TMUX_HUD_LEADER_PANE = options.leaderPaneId ?? "";
|
|
4015
|
+
const runtimeContext = options.runtimeContext;
|
|
4016
|
+
const verifiedMadmaxContext = options.verifiedMadmaxContext;
|
|
4017
|
+
if (runtimeContext && verifiedMadmaxContext) {
|
|
4018
|
+
values.OMX_ROOT = verifiedMadmaxContext.root;
|
|
4019
|
+
values.OMXBOX_ACTIVE = "1";
|
|
4020
|
+
values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
|
|
4021
|
+
values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
|
|
4022
|
+
values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
|
|
4023
|
+
}
|
|
4024
|
+
else {
|
|
4025
|
+
const madmaxGuardPresent = env.OMXBOX_ACTIVE === "1";
|
|
4026
|
+
const teamRoot = env.OMX_TEAM_STATE_ROOT?.trim();
|
|
4027
|
+
const root = env.OMX_ROOT?.trim();
|
|
4028
|
+
const stateRoot = env.OMX_STATE_ROOT?.trim();
|
|
4029
|
+
const resolvedStateRoot = stateRoot ? resolveDetachedRootValue(options.cwd, stateRoot) : undefined;
|
|
4030
|
+
const explicitOverrideWins = Boolean(options.omxRootOverride?.trim()
|
|
4031
|
+
&& !teamRoot
|
|
4032
|
+
&& !root
|
|
4033
|
+
&& (!resolvedStateRoot || resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim()) !== resolvedStateRoot));
|
|
4034
|
+
if (teamRoot)
|
|
4035
|
+
values.OMX_TEAM_STATE_ROOT = resolveDetachedRootValue(options.cwd, teamRoot);
|
|
4036
|
+
if (!madmaxGuardPresent) {
|
|
4037
|
+
if (explicitOverrideWins)
|
|
4038
|
+
values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
|
|
4039
|
+
else if (!teamRoot && root)
|
|
4040
|
+
values.OMX_ROOT = resolveDetachedRootValue(options.cwd, root);
|
|
4041
|
+
else if (!teamRoot && stateRoot)
|
|
4042
|
+
values.OMX_STATE_ROOT = resolvedStateRoot;
|
|
4043
|
+
else if (!teamRoot && options.omxRootOverride?.trim())
|
|
4044
|
+
values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
|
|
4045
|
+
}
|
|
4046
|
+
if (verifiedMadmaxContext) {
|
|
4047
|
+
values.OMX_ROOT = verifiedMadmaxContext.root;
|
|
4048
|
+
values.OMX_STATE_ROOT = "";
|
|
4049
|
+
values.OMX_TEAM_STATE_ROOT = "";
|
|
4050
|
+
values.OMXBOX_ACTIVE = "1";
|
|
4051
|
+
values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
|
|
4052
|
+
values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
|
|
4053
|
+
values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
const verifiedTeam = options.verifiedTeamContext;
|
|
4057
|
+
if (verifiedTeam && !verifiedMadmaxContext) {
|
|
4058
|
+
values.OMX_ROOT = "";
|
|
4059
|
+
values.OMX_STATE_ROOT = "";
|
|
4060
|
+
values.OMXBOX_ACTIVE = "";
|
|
4061
|
+
values.OMX_SOURCE_CWD = "";
|
|
4062
|
+
values.OMX_MADMAX_DETACHED_CONTEXT = "";
|
|
4063
|
+
values.OMX_RUNS_DIR = "";
|
|
4064
|
+
values.OMX_TEAM_STATE_ROOT = verifiedTeam.stateRoot;
|
|
4065
|
+
values.OMX_TEAM_LEADER_CWD = verifiedTeam.leaderCwd;
|
|
4066
|
+
values.OMX_TEAM_WORKER = verifiedTeam.worker;
|
|
4067
|
+
values.OMX_TEAM_INTERNAL_WORKER = verifiedTeam.internalWorker;
|
|
4068
|
+
}
|
|
4069
|
+
return Object.freeze({
|
|
4070
|
+
values: Object.freeze(values),
|
|
4071
|
+
keys: DETACHED_LAUNCH_CONTROL_PLANE_KEYS,
|
|
4072
|
+
});
|
|
3365
4073
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
4074
|
+
export const resolveDetachedLaunchControlPlane = buildDetachedLaunchControlPlane;
|
|
4075
|
+
function detachedLaunchControlPlaneEnv(controlPlane) {
|
|
4076
|
+
return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, controlPlane.values[key]]));
|
|
4077
|
+
}
|
|
4078
|
+
function applyLaunchOwnedControlPlane(controlPlane) {
|
|
4079
|
+
const previous = new Map();
|
|
4080
|
+
for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
|
|
4081
|
+
previous.set(key, process.env[key]);
|
|
4082
|
+
const value = controlPlane.values[key];
|
|
4083
|
+
if (value === "")
|
|
4084
|
+
delete process.env[key];
|
|
4085
|
+
else
|
|
4086
|
+
process.env[key] = value;
|
|
4087
|
+
}
|
|
4088
|
+
return () => {
|
|
4089
|
+
for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
|
|
4090
|
+
const value = previous.get(key);
|
|
4091
|
+
if (typeof value === "string")
|
|
4092
|
+
process.env[key] = value;
|
|
4093
|
+
else
|
|
4094
|
+
delete process.env[key];
|
|
4095
|
+
}
|
|
4096
|
+
};
|
|
3368
4097
|
}
|
|
3369
4098
|
const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3370
4099
|
const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
|
|
@@ -3376,21 +4105,23 @@ const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
|
|
|
3376
4105
|
"COLUMNS",
|
|
3377
4106
|
"LINES",
|
|
3378
4107
|
]);
|
|
3379
|
-
|
|
3380
|
-
const
|
|
4108
|
+
function detachedSessionParentEnvironment(env, caseInsensitive = false) {
|
|
4109
|
+
const values = {};
|
|
3381
4110
|
for (const key of Object.keys(env).sort()) {
|
|
3382
|
-
if (!SHELL_ENV_NAME_PATTERN.test(key)
|
|
3383
|
-
|
|
3384
|
-
|
|
4111
|
+
if (!SHELL_ENV_NAME_PATTERN.test(key)
|
|
4112
|
+
|| DETACHED_SESSION_PANE_ENV_KEYS.has(key)
|
|
4113
|
+
|| isDetachedLaunchControlPlaneKey(key, caseInsensitive))
|
|
3385
4114
|
continue;
|
|
3386
4115
|
const value = env[key];
|
|
3387
|
-
if (typeof value
|
|
3388
|
-
|
|
3389
|
-
if (value.includes("\0"))
|
|
3390
|
-
continue;
|
|
3391
|
-
lines.push(`export ${key}=${quoteShellArg(value)}`);
|
|
4116
|
+
if (typeof value === "string" && !value.includes("\0"))
|
|
4117
|
+
values[key] = value;
|
|
3392
4118
|
}
|
|
3393
|
-
return
|
|
4119
|
+
return values;
|
|
4120
|
+
}
|
|
4121
|
+
export function serializeDetachedSessionParentEnv(env) {
|
|
4122
|
+
return `${Object.entries(detachedSessionParentEnvironment(env))
|
|
4123
|
+
.map(([key, value]) => `export ${key}=${quoteShellArg(value)}`)
|
|
4124
|
+
.join("\n")}\n`;
|
|
3394
4125
|
}
|
|
3395
4126
|
export function detachedSessionParentEnvFilePath(cwd, sessionId) {
|
|
3396
4127
|
const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
@@ -3418,27 +4149,21 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
|
|
|
3418
4149
|
releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
|
|
3419
4150
|
}
|
|
3420
4151
|
}
|
|
3421
|
-
export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel
|
|
4152
|
+
export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin = process.argv[1], preLaunchOptions = {
|
|
4153
|
+
enableNotifyFallbackAuthority: false,
|
|
4154
|
+
worktreeDirty: false,
|
|
4155
|
+
shouldAttach: true,
|
|
4156
|
+
}, controlPlane) {
|
|
3422
4157
|
const detachedLeaderCmd = nativeWindows
|
|
3423
|
-
?
|
|
3424
|
-
: buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
|
|
3425
|
-
const
|
|
3426
|
-
|
|
3427
|
-
:
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
: hasExplicitRootOverride
|
|
3433
|
-
? {
|
|
3434
|
-
omxRoot: omxRootOverride,
|
|
3435
|
-
rootSource: resolveHudRuntimeRootSource(omxRootOverride, env),
|
|
3436
|
-
}
|
|
3437
|
-
: resolveHudRuntimeRootForLaunch(cwd, env);
|
|
3438
|
-
const hudRuntimeEnv = buildHudRuntimeEnv({
|
|
3439
|
-
sessionId,
|
|
3440
|
-
...hudRuntimeRoot,
|
|
3441
|
-
}).env;
|
|
4158
|
+
? buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, env, releaseMarkerPath, preLaunchOptions, omxBin)
|
|
4159
|
+
: buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, releaseMarkerPath, preLaunchOptions, omxBin);
|
|
4160
|
+
const launchControlPlane = controlPlane ?? buildDetachedLaunchControlPlane({
|
|
4161
|
+
cwd,
|
|
4162
|
+
sessionId: sessionId ?? "",
|
|
4163
|
+
env,
|
|
4164
|
+
omxRootOverride,
|
|
4165
|
+
});
|
|
4166
|
+
const controlPlaneEnv = detachedLaunchControlPlaneEnv(launchControlPlane);
|
|
3442
4167
|
const newSessionArgs = [
|
|
3443
4168
|
"new-session",
|
|
3444
4169
|
"-d",
|
|
@@ -3449,15 +4174,10 @@ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, h
|
|
|
3449
4174
|
sessionName,
|
|
3450
4175
|
"-c",
|
|
3451
4176
|
cwd,
|
|
4177
|
+
...DETACHED_LAUNCH_CONTROL_PLANE_KEYS.flatMap((key) => ["-e", `${key}=${controlPlaneEnv[key]}`]),
|
|
3452
4178
|
...(workerLaunchArgs ? ["-e", `${TEAM_WORKER_LAUNCH_ARGS_ENV}=${workerLaunchArgs}`] : []),
|
|
3453
|
-
...Object.entries(hudRuntimeEnv).map(([key, value]) => ["-e", `${key}=${value}`]).flat(),
|
|
3454
4179
|
...(codexHomeOverride ? ["-e", `CODEX_HOME=${codexHomeOverride}`] : []),
|
|
3455
4180
|
...(sqliteHomeOverride ? ["-e", `${CODEX_SQLITE_HOME_ENV}=${sqliteHomeOverride}`] : []),
|
|
3456
|
-
...(env.OMXBOX_ACTIVE ? ["-e", `OMXBOX_ACTIVE=${env.OMXBOX_ACTIVE}`] : []),
|
|
3457
|
-
...(env.OMX_SOURCE_CWD ? ["-e", `OMX_SOURCE_CWD=${env.OMX_SOURCE_CWD}`] : []),
|
|
3458
|
-
...(env[OMX_MADMAX_DETACHED_CONTEXT_ENV]
|
|
3459
|
-
? ["-e", `${OMX_MADMAX_DETACHED_CONTEXT_ENV}=${env[OMX_MADMAX_DETACHED_CONTEXT_ENV]}`]
|
|
3460
|
-
: []),
|
|
3461
4181
|
...(notifyTempContractRaw
|
|
3462
4182
|
? ["-e", `${OMX_NOTIFY_TEMP_CONTRACT_ENV}=${notifyTempContractRaw}`]
|
|
3463
4183
|
: []),
|
|
@@ -3717,41 +4437,40 @@ function isAutopilotReviewPendingPostLaunchState(state) {
|
|
|
3717
4437
|
function postLaunchUniqueStrings(values) {
|
|
3718
4438
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
3719
4439
|
}
|
|
3720
|
-
async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso,
|
|
4440
|
+
async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeRootStateFn) {
|
|
3721
4441
|
const normalizedSessionId = cleanPostLaunchString(sessionId);
|
|
3722
4442
|
if (!normalizedSessionId)
|
|
3723
4443
|
return;
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
return
|
|
3738
|
-
|
|
4444
|
+
await writeRootStateFn(stateDir, (rootState) => {
|
|
4445
|
+
if (!rootState)
|
|
4446
|
+
return null;
|
|
4447
|
+
const rootSessionIds = postLaunchUniqueStrings([
|
|
4448
|
+
cleanPostLaunchString(rootState.session_id),
|
|
4449
|
+
cleanPostLaunchString(extractSessionIdFromInitializedStatePath(rootState.initialized_state_path)),
|
|
4450
|
+
]);
|
|
4451
|
+
const rootBelongsToSession = rootSessionIds.includes(normalizedSessionId);
|
|
4452
|
+
const entries = listActiveSkills(rootState);
|
|
4453
|
+
const keptEntries = entries.filter((entry) => {
|
|
4454
|
+
const entrySessionId = cleanPostLaunchString(entry.session_id);
|
|
4455
|
+
if (entrySessionId)
|
|
4456
|
+
return entrySessionId !== normalizedSessionId;
|
|
4457
|
+
return !rootBelongsToSession;
|
|
4458
|
+
});
|
|
4459
|
+
if (keptEntries.length === entries.length && rootState.active !== true)
|
|
4460
|
+
return null;
|
|
4461
|
+
if (keptEntries.length === entries.length && !rootBelongsToSession)
|
|
4462
|
+
return null;
|
|
4463
|
+
return {
|
|
4464
|
+
...rootState,
|
|
4465
|
+
active: keptEntries.length > 0,
|
|
4466
|
+
skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
|
|
4467
|
+
phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
|
|
4468
|
+
updated_at: nowIso,
|
|
4469
|
+
active_skills: keptEntries,
|
|
4470
|
+
post_launch_reconciled_at: nowIso,
|
|
4471
|
+
post_launch_reconciliation_reason: "terminal_session_cleanup",
|
|
4472
|
+
};
|
|
3739
4473
|
});
|
|
3740
|
-
if (keptEntries.length === entries.length && rootState.active !== true)
|
|
3741
|
-
return;
|
|
3742
|
-
if (keptEntries.length === entries.length && !rootBelongsToSession)
|
|
3743
|
-
return;
|
|
3744
|
-
const nextRoot = {
|
|
3745
|
-
...rootState,
|
|
3746
|
-
active: keptEntries.length > 0,
|
|
3747
|
-
skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
|
|
3748
|
-
phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
|
|
3749
|
-
updated_at: nowIso,
|
|
3750
|
-
active_skills: keptEntries,
|
|
3751
|
-
post_launch_reconciled_at: nowIso,
|
|
3752
|
-
post_launch_reconciliation_reason: "terminal_session_cleanup",
|
|
3753
|
-
};
|
|
3754
|
-
await writeFileFn(rootPath, JSON.stringify(nextRoot, null, 2));
|
|
3755
4474
|
}
|
|
3756
4475
|
function buildRecoveredPostLaunchModeState(mode, completedAt) {
|
|
3757
4476
|
return {
|
|
@@ -3798,9 +4517,14 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3798
4517
|
? [getStateDir(cwd, sessionId)]
|
|
3799
4518
|
: [getBaseStateDir(cwd)];
|
|
3800
4519
|
const rootStateDir = getBaseStateDir(cwd);
|
|
3801
|
-
const
|
|
3802
|
-
|
|
3803
|
-
|
|
4520
|
+
const writeRootState = dependencies.writeRootState ?? ((stateDir, update) => (updateRootSkillActiveStateForStateDir(stateDir, update)));
|
|
4521
|
+
const writeModeState = async (stateDir, mode, path, state) => {
|
|
4522
|
+
if (stateDir === rootStateDir && mode === SKILL_ACTIVE_STATE_MODE) {
|
|
4523
|
+
await writeRootState(stateDir, () => state);
|
|
4524
|
+
return;
|
|
4525
|
+
}
|
|
4526
|
+
await writeFile(path, JSON.stringify(state, null, 2));
|
|
4527
|
+
};
|
|
3804
4528
|
let preserveSkillActiveForReviewPendingAutopilot = false;
|
|
3805
4529
|
for (const stateDir of scopedDirs) {
|
|
3806
4530
|
const files = await readdir(stateDir).catch(() => []);
|
|
@@ -3812,7 +4536,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3812
4536
|
&& isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
|
|
3813
4537
|
preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
|
|
3814
4538
|
for (const file of files) {
|
|
3815
|
-
if (!
|
|
4539
|
+
if (!isModeStateFilename(file))
|
|
3816
4540
|
continue;
|
|
3817
4541
|
const path = join(stateDir, file);
|
|
3818
4542
|
const mode = file.slice(0, -"-state.json".length);
|
|
@@ -3821,9 +4545,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3821
4545
|
if (result.kind === "recoverable") {
|
|
3822
4546
|
try {
|
|
3823
4547
|
const completedAt = now().toISOString();
|
|
3824
|
-
|
|
4548
|
+
const recoveredState = mode === SKILL_ACTIVE_STATE_MODE
|
|
3825
4549
|
? buildRecoveredPostLaunchSkillActiveState(completedAt)
|
|
3826
|
-
: buildRecoveredPostLaunchModeState(mode, completedAt)
|
|
4550
|
+
: buildRecoveredPostLaunchModeState(mode, completedAt);
|
|
4551
|
+
await writeModeState(stateDir, mode, path, recoveredState);
|
|
3827
4552
|
if (isTrackedWorkflowMode(mode)) {
|
|
3828
4553
|
await syncCanonicalSkillStateForMode({
|
|
3829
4554
|
cwd,
|
|
@@ -3856,12 +4581,12 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3856
4581
|
: normalizeTerminalWorkflowState(result.state, { mode, nowIso: completedAt });
|
|
3857
4582
|
if (normalized.changed) {
|
|
3858
4583
|
result.state = normalized.state;
|
|
3859
|
-
await
|
|
4584
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3860
4585
|
}
|
|
3861
4586
|
if (mode === "ralph") {
|
|
3862
4587
|
const completedAt = now().toISOString();
|
|
3863
4588
|
if (markRalphCompletionAuditBlockedForPostLaunch(result.state, cwd, completedAt)) {
|
|
3864
|
-
await
|
|
4589
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3865
4590
|
await syncCanonicalSkillStateForMode({
|
|
3866
4591
|
cwd,
|
|
3867
4592
|
baseStateDir: rootStateDir,
|
|
@@ -3887,7 +4612,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3887
4612
|
result.state.phase = "complete";
|
|
3888
4613
|
result.state.updated_at = completedAt;
|
|
3889
4614
|
result.state.active_skills = [];
|
|
3890
|
-
await
|
|
4615
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3891
4616
|
continue;
|
|
3892
4617
|
}
|
|
3893
4618
|
result.state.active = false;
|
|
@@ -3898,7 +4623,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3898
4623
|
result.state.interrupted_at = completedAt;
|
|
3899
4624
|
result.state.stop_reason = cleanPostLaunchString(result.state.stop_reason) || "session_exit";
|
|
3900
4625
|
}
|
|
3901
|
-
await
|
|
4626
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3902
4627
|
if (isTrackedWorkflowMode(mode)) {
|
|
3903
4628
|
await syncCanonicalSkillStateForMode({
|
|
3904
4629
|
cwd,
|
|
@@ -3920,7 +4645,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3920
4645
|
if (sessionId) {
|
|
3921
4646
|
try {
|
|
3922
4647
|
if (!preserveSkillActiveForReviewPendingAutopilot) {
|
|
3923
|
-
await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(),
|
|
4648
|
+
await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeRootState);
|
|
3924
4649
|
}
|
|
3925
4650
|
}
|
|
3926
4651
|
catch (err) {
|
|
@@ -3946,115 +4671,140 @@ export async function reapPostLaunchOrphanedMcpProcesses(dependencies = {}) {
|
|
|
3946
4671
|
writeError(`[cli/index] postLaunch MCP cleanup failed: ${err}\n`);
|
|
3947
4672
|
}
|
|
3948
4673
|
}
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4674
|
+
async function establishPreLaunchBinding(cwd, sessionId, metadata = {}) {
|
|
4675
|
+
const pointerOptions = await resolvePreLaunchSessionPointerOptions();
|
|
4676
|
+
const established = await establishLaunchSessionBinding(cwd, sessionId, { ...pointerOptions, ...metadata });
|
|
4677
|
+
if (established.kind === "committed-released")
|
|
4678
|
+
return { binding: established.binding, cleanup: established.cleanup };
|
|
4679
|
+
const error = established.kind === "precommit-aborted" ? established.abort : established.error;
|
|
4680
|
+
Object.assign(error, { establishmentCleanup: established.cleanup });
|
|
4681
|
+
throw error;
|
|
4682
|
+
}
|
|
4683
|
+
async function failPreLaunchSetup(binding) {
|
|
4684
|
+
const failures = [];
|
|
4685
|
+
try {
|
|
4686
|
+
await finalizeBoundOnce(binding, "setup-failure");
|
|
4687
|
+
}
|
|
4688
|
+
catch (error) {
|
|
4689
|
+
failures.push(error);
|
|
4690
|
+
}
|
|
4691
|
+
finally {
|
|
4692
|
+
try {
|
|
4693
|
+
const close = await closeLaunchSessionBindingOnce(binding);
|
|
4694
|
+
if (close.status === "failed")
|
|
4695
|
+
failures.push(close.error ?? new Error("bound session close failed"));
|
|
4696
|
+
}
|
|
4697
|
+
catch (error) {
|
|
4698
|
+
failures.push(error);
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
if (failures.length > 0)
|
|
4702
|
+
throw new AggregateError(failures, "preLaunch setup finalization failed");
|
|
4703
|
+
}
|
|
4704
|
+
async function completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty) {
|
|
4705
|
+
const degraded = [];
|
|
3961
4706
|
try {
|
|
3962
4707
|
const cleanup = await cleanupLaunchOrphanedMcpProcesses();
|
|
3963
|
-
if (cleanup.terminatedCount > 0)
|
|
4708
|
+
if (cleanup.terminatedCount > 0)
|
|
3964
4709
|
console.log(`[omx] Reaped ${cleanup.terminatedCount} orphaned OMX MCP process(es) before launch.`);
|
|
3965
|
-
|
|
3966
|
-
if (cleanup.failedPids.length > 0) {
|
|
4710
|
+
if (cleanup.failedPids.length > 0)
|
|
3967
4711
|
console.warn(`[omx] Failed to reap ${cleanup.failedPids.length} orphaned OMX MCP process(es); continuing launch.`);
|
|
3968
|
-
}
|
|
3969
4712
|
}
|
|
3970
|
-
catch (
|
|
3971
|
-
|
|
3972
|
-
|
|
4713
|
+
catch (error) {
|
|
4714
|
+
degraded.push("orphan-reaping");
|
|
4715
|
+
logCliOperationFailure(error);
|
|
4716
|
+
}
|
|
4717
|
+
let instructions;
|
|
4718
|
+
try {
|
|
4719
|
+
const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
|
|
4720
|
+
const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
|
|
4721
|
+
const launchAppendix = await readLaunchAppendInstructions();
|
|
4722
|
+
const dirtyWorktreeGuidance = worktreeDirty
|
|
4723
|
+
? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
|
|
4724
|
+
: "";
|
|
4725
|
+
instructions = launchAppendix.trim().length > 0 ? `${overlay}\n\n${launchAppendix}${dirtyWorktreeGuidance}` : `${overlay}${dirtyWorktreeGuidance}`;
|
|
4726
|
+
}
|
|
4727
|
+
catch (error) {
|
|
4728
|
+
return { kind: "failure", operation: "overlay", error };
|
|
4729
|
+
}
|
|
4730
|
+
try {
|
|
4731
|
+
await writeSessionModelInstructionsFile(cwd, sessionId, instructions);
|
|
4732
|
+
}
|
|
4733
|
+
catch (error) {
|
|
4734
|
+
return { kind: "failure", operation: "session-instructions", error };
|
|
4735
|
+
}
|
|
4736
|
+
try {
|
|
4737
|
+
await resetSessionMetrics(cwd, sessionId);
|
|
4738
|
+
tagCurrentTmuxSessionWithInstance(sessionId);
|
|
4739
|
+
}
|
|
4740
|
+
catch (error) {
|
|
4741
|
+
return { kind: "failure", operation: "session-metrics", error };
|
|
3973
4742
|
}
|
|
3974
|
-
// 2. Establish the canonical pointer before any session-scoped launch artifact.
|
|
3975
|
-
await writeSessionStart(cwd, sessionId, await resolvePreLaunchSessionPointerOptions());
|
|
3976
|
-
// 3. Generate runtime overlay + write session-scoped model instructions file
|
|
3977
|
-
const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
|
|
3978
|
-
const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
|
|
3979
|
-
const launchAppendix = await readLaunchAppendInstructions();
|
|
3980
|
-
const dirtyWorktreeGuidance = worktreeDirty
|
|
3981
|
-
? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
|
|
3982
|
-
: "";
|
|
3983
|
-
const sessionInstructions = launchAppendix.trim().length > 0
|
|
3984
|
-
? `${overlay}
|
|
3985
|
-
|
|
3986
|
-
${launchAppendix}${dirtyWorktreeGuidance}`
|
|
3987
|
-
: `${overlay}${dirtyWorktreeGuidance}`;
|
|
3988
|
-
await writeSessionModelInstructionsFile(cwd, sessionId, sessionInstructions);
|
|
3989
|
-
// 4. Reset session metrics and tag the established session.
|
|
3990
|
-
await resetSessionMetrics(cwd, sessionId);
|
|
3991
|
-
tagCurrentTmuxSessionWithInstance(sessionId);
|
|
3992
|
-
// 5. Start notify fallback watcher (best effort)
|
|
3993
4743
|
try {
|
|
3994
4744
|
await startNotifyFallbackWatcher(cwd, { codexHomeOverride, enableAuthority: enableNotifyFallbackAuthority, sessionId });
|
|
3995
4745
|
}
|
|
3996
|
-
catch (
|
|
3997
|
-
|
|
3998
|
-
|
|
4746
|
+
catch (error) {
|
|
4747
|
+
degraded.push("notify-watcher");
|
|
4748
|
+
logCliOperationFailure(error);
|
|
3999
4749
|
}
|
|
4000
|
-
// 6. Start derived watcher (best effort, opt-in)
|
|
4001
4750
|
try {
|
|
4002
4751
|
await startHookDerivedWatcher(cwd);
|
|
4003
4752
|
}
|
|
4004
|
-
catch (
|
|
4005
|
-
|
|
4006
|
-
|
|
4753
|
+
catch (error) {
|
|
4754
|
+
degraded.push("derived-watcher");
|
|
4755
|
+
logCliOperationFailure(error);
|
|
4007
4756
|
}
|
|
4008
|
-
// 7. Emit temp notification startup summary + warnings, then send session-start lifecycle notification (best effort)
|
|
4009
4757
|
try {
|
|
4010
4758
|
if (notifyTempContract?.active) {
|
|
4011
|
-
process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] =
|
|
4012
|
-
serializeNotifyTempContract(notifyTempContract);
|
|
4759
|
+
process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] = serializeNotifyTempContract(notifyTempContract);
|
|
4013
4760
|
const { getNotificationConfig } = await import("../notifications/config.js");
|
|
4014
|
-
const
|
|
4015
|
-
const
|
|
4016
|
-
for (const info of startup.infoLines) {
|
|
4761
|
+
const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(getNotificationConfig()?.enabled));
|
|
4762
|
+
for (const info of startup.infoLines)
|
|
4017
4763
|
console.log(`[omx] ${info}`);
|
|
4018
|
-
|
|
4019
|
-
for (const warning of startup.warningLines) {
|
|
4764
|
+
for (const warning of startup.warningLines)
|
|
4020
4765
|
console.warn(`[omx] ${warning}`);
|
|
4021
|
-
}
|
|
4022
4766
|
}
|
|
4023
|
-
else
|
|
4767
|
+
else
|
|
4024
4768
|
delete process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV];
|
|
4025
|
-
}
|
|
4026
4769
|
const { notifyLifecycle } = await import("../notifications/index.js");
|
|
4027
|
-
await notifyLifecycle("session-start", {
|
|
4028
|
-
sessionId,
|
|
4029
|
-
projectPath: cwd,
|
|
4030
|
-
projectName: basename(cwd),
|
|
4031
|
-
});
|
|
4770
|
+
await notifyLifecycle("session-start", { sessionId, projectPath: cwd, projectName: basename(cwd) });
|
|
4032
4771
|
}
|
|
4033
|
-
catch (
|
|
4034
|
-
|
|
4035
|
-
|
|
4772
|
+
catch (error) {
|
|
4773
|
+
degraded.push("notification");
|
|
4774
|
+
logCliOperationFailure(error);
|
|
4036
4775
|
}
|
|
4037
|
-
// 8. Dispatch native hook event (best effort)
|
|
4038
4776
|
try {
|
|
4039
|
-
await emitNativeHookEvent(cwd, "session-start", {
|
|
4040
|
-
session_id: sessionId,
|
|
4041
|
-
context: buildNativeHookBaseContext(cwd, sessionId, "started", {
|
|
4042
|
-
project_path: cwd,
|
|
4043
|
-
project_name: basename(cwd),
|
|
4044
|
-
status: "started",
|
|
4045
|
-
}),
|
|
4046
|
-
});
|
|
4777
|
+
await emitNativeHookEvent(cwd, "session-start", { session_id: sessionId, context: buildNativeHookBaseContext(cwd, sessionId, "started", { project_path: cwd, project_name: basename(cwd), status: "started" }) });
|
|
4047
4778
|
}
|
|
4048
|
-
catch (
|
|
4049
|
-
|
|
4050
|
-
|
|
4779
|
+
catch (error) {
|
|
4780
|
+
degraded.push("native-lifecycle-event");
|
|
4781
|
+
logCliOperationFailure(error);
|
|
4782
|
+
}
|
|
4783
|
+
return degraded.length === 0 ? { kind: "success" } : { kind: "degraded-success", operations: degraded };
|
|
4784
|
+
}
|
|
4785
|
+
export async function preLaunch(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority = false, worktreeDirty = false) {
|
|
4786
|
+
const established = await establishPreLaunchBinding(cwd, sessionId);
|
|
4787
|
+
const completion = {
|
|
4788
|
+
...await completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty),
|
|
4789
|
+
establishmentCleanup: established.cleanup,
|
|
4790
|
+
};
|
|
4791
|
+
if (completion.kind === "failure") {
|
|
4792
|
+
try {
|
|
4793
|
+
await failPreLaunchSetup(established.binding);
|
|
4794
|
+
}
|
|
4795
|
+
catch (cleanupError) {
|
|
4796
|
+
throw new AggregateError([completion.error, cleanupError], `preLaunch ${completion.operation} failed`);
|
|
4797
|
+
}
|
|
4798
|
+
throw completion.error;
|
|
4051
4799
|
}
|
|
4800
|
+
return { binding: established.binding, completion };
|
|
4052
4801
|
}
|
|
4053
4802
|
/**
|
|
4054
4803
|
* runCodex: Launch Codex CLI (blocks until exit).
|
|
4055
4804
|
* All 3 paths (new tmux, existing tmux, no tmux) block via execSync/execFileSync.
|
|
4056
4805
|
*/
|
|
4057
|
-
function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw,
|
|
4806
|
+
async function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, preLaunchOptions, launchPolicy = "direct", detachedPreflight, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, runtimeContext, verifiedMadmaxContext, launchControlPlane) {
|
|
4807
|
+
void preLaunchOptions;
|
|
4058
4808
|
const launchArgs = injectModelInstructionsBypassArgs(cwd, args, process.env, sessionModelInstructionsPath(cwd, sessionId));
|
|
4059
4809
|
const nativeWindows = isNativeWindows();
|
|
4060
4810
|
const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
|
|
@@ -4062,19 +4812,59 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4062
4812
|
throw new Error("Unable to resolve OMX launcher path for tmux HUD bootstrap");
|
|
4063
4813
|
}
|
|
4064
4814
|
const runtimeEnvOverlay = buildMadmaxWorktreeRuntimeEnvOverlay(runtimeContext);
|
|
4815
|
+
let verifiedTeamContext;
|
|
4816
|
+
if (launchPolicy === "detached-tmux") {
|
|
4817
|
+
try {
|
|
4818
|
+
verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
|
|
4819
|
+
}
|
|
4820
|
+
catch {
|
|
4821
|
+
verifiedTeamContext = undefined;
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
const verifiedMadmaxContextForLaunch = verifiedMadmaxContext ?? (runtimeContext?.madmaxDetachedContext
|
|
4825
|
+
? {
|
|
4826
|
+
root: runtimeContext.omxRoot,
|
|
4827
|
+
sourceCwd: runtimeContext.sourceCwd,
|
|
4828
|
+
context: runtimeContext.madmaxDetachedContext,
|
|
4829
|
+
runsRoot: resolveMadmaxRunsRoot(process.env),
|
|
4830
|
+
}
|
|
4831
|
+
: resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, process.env));
|
|
4832
|
+
const detachedControlPlane = launchPolicy === "detached-tmux"
|
|
4833
|
+
? buildDetachedLaunchControlPlane({
|
|
4834
|
+
cwd,
|
|
4835
|
+
sessionId,
|
|
4836
|
+
env: process.env,
|
|
4837
|
+
omxRootOverride: runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env),
|
|
4838
|
+
runtimeContext,
|
|
4839
|
+
verifiedMadmaxContext: verifiedMadmaxContextForLaunch,
|
|
4840
|
+
verifiedTeamContext,
|
|
4841
|
+
})
|
|
4842
|
+
: undefined;
|
|
4843
|
+
const launchOwnedControlPlane = detachedControlPlane ?? launchControlPlane;
|
|
4844
|
+
const launchOwnedControlPlaneEnv = launchOwnedControlPlane
|
|
4845
|
+
? detachedLaunchControlPlaneEnv(launchOwnedControlPlane)
|
|
4846
|
+
: {};
|
|
4847
|
+
const detachedSelectedStateEnv = detachedControlPlane
|
|
4848
|
+
? { ...process.env, ...detachedLaunchControlPlaneEnv(detachedControlPlane) }
|
|
4849
|
+
: undefined;
|
|
4065
4850
|
const omxRootOverride = runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env);
|
|
4851
|
+
const selectedOmxRootOverride = launchOwnedControlPlane
|
|
4852
|
+
? launchOwnedControlPlane.values.OMX_ROOT || undefined
|
|
4853
|
+
: omxRootOverride;
|
|
4066
4854
|
const currentPaneId = process.env.TMUX_PANE;
|
|
4067
4855
|
const hudRuntimeRoot = runtimeContext
|
|
4068
4856
|
? { omxRoot: runtimeContext.omxRoot, rootSource: 'omx-root-env' }
|
|
4069
4857
|
: resolveHudRuntimeRootForLaunch(cwd, process.env);
|
|
4070
|
-
const hudRuntimeEnv =
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4858
|
+
const hudRuntimeEnv = launchOwnedControlPlane
|
|
4859
|
+
? Object.fromEntries(Object.entries(launchOwnedControlPlaneEnv))
|
|
4860
|
+
: {
|
|
4861
|
+
...buildHudRuntimeEnv({
|
|
4862
|
+
sessionId,
|
|
4863
|
+
leaderPaneId: currentPaneId,
|
|
4864
|
+
...hudRuntimeRoot,
|
|
4865
|
+
}).env,
|
|
4866
|
+
...runtimeEnvOverlay,
|
|
4867
|
+
};
|
|
4078
4868
|
const hudEnvArgs = Object.entries(hudRuntimeEnv).map(([key, value]) => `${key}=${value}`);
|
|
4079
4869
|
const hudCmd = nativeWindows
|
|
4080
4870
|
? buildWindowsPromptCommand("node", [omxBin, "hud", "--watch"])
|
|
@@ -4089,13 +4879,17 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4089
4879
|
...stripHermesMcpBridgeEnv(process.env),
|
|
4090
4880
|
...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
|
|
4091
4881
|
...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
|
|
4092
|
-
...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
4882
|
+
...(!launchOwnedControlPlane && omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
4093
4883
|
...runtimeEnvOverlay,
|
|
4884
|
+
...launchOwnedControlPlaneEnv,
|
|
4094
4885
|
}, omxBin);
|
|
4886
|
+
// Correlates plugin hook routing only; it is not an authority token.
|
|
4887
|
+
const pluginHookRoutingLaunchId = randomUUID();
|
|
4095
4888
|
const codexEnvWithSession = {
|
|
4096
4889
|
...codexBaseEnv,
|
|
4097
|
-
OMX_CODEX_LAUNCH_ID:
|
|
4890
|
+
OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
|
|
4098
4891
|
...buildHudRuntimeEnv({ sessionId }).env,
|
|
4892
|
+
...launchOwnedControlPlaneEnv,
|
|
4099
4893
|
};
|
|
4100
4894
|
const codexEnv = workerLaunchArgs
|
|
4101
4895
|
? {
|
|
@@ -4107,8 +4901,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4107
4901
|
const codexEnvWithNotify = notifyTempContractRaw
|
|
4108
4902
|
? { ...codexEnv, [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw }
|
|
4109
4903
|
: codexEnv;
|
|
4110
|
-
const runtimeHookEnv =
|
|
4111
|
-
|
|
4904
|
+
const runtimeHookEnv = launchOwnedControlPlane
|
|
4905
|
+
? { ...process.env, ...runtimeEnvOverlay, ...launchOwnedControlPlaneEnv }
|
|
4906
|
+
: { ...process.env, ...runtimeEnvOverlay };
|
|
4112
4907
|
if (isCodexVersionRequest(launchArgs)) {
|
|
4113
4908
|
runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
|
|
4114
4909
|
return { postLaunchHandledExternally: false };
|
|
@@ -4144,7 +4939,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4144
4939
|
currentPaneId,
|
|
4145
4940
|
cwd,
|
|
4146
4941
|
sessionId,
|
|
4147
|
-
omxRootOverride,
|
|
4942
|
+
omxRootOverride: selectedOmxRootOverride,
|
|
4148
4943
|
baseEnv: runtimeHookEnv,
|
|
4149
4944
|
});
|
|
4150
4945
|
}
|
|
@@ -4171,7 +4966,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4171
4966
|
currentPaneId,
|
|
4172
4967
|
cwd,
|
|
4173
4968
|
sessionId,
|
|
4174
|
-
omxRootOverride,
|
|
4969
|
+
omxRootOverride: selectedOmxRootOverride,
|
|
4175
4970
|
baseEnv: runtimeHookEnv,
|
|
4176
4971
|
});
|
|
4177
4972
|
}
|
|
@@ -4232,214 +5027,251 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4232
5027
|
return { postLaunchHandledExternally: false };
|
|
4233
5028
|
}
|
|
4234
5029
|
else {
|
|
4235
|
-
|
|
5030
|
+
if (!detachedPreflight) {
|
|
5031
|
+
throw new DetachedLaunchSafetyError("establishment", new Error("detached launch requires a frozen available preflight"), { transitions: ["D0"], rollback: { attempted: [], failures: [] } });
|
|
5032
|
+
}
|
|
4236
5033
|
const codexCmd = buildTmuxPaneCommand("codex", launchArgs);
|
|
4237
5034
|
const detachedWindowsCodexCmd = nativeWindows
|
|
4238
|
-
?
|
|
5035
|
+
? buildWindowsDetachedChildCommand("codex", launchArgs)
|
|
4239
5036
|
: null;
|
|
4240
5037
|
const sessionName = buildDetachedTmuxSessionName(cwd, sessionId);
|
|
4241
|
-
const
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
if (!
|
|
4256
|
-
clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
|
|
4257
|
-
process.stderr.write(`[omx] madmax detached launch already active for this context; reusing ${activeRecord.tmux_session_name} without attaching because this launch is a Hermes MCP bridge.\n`);
|
|
4258
|
-
return { postLaunchHandledExternally: true };
|
|
4259
|
-
}
|
|
4260
|
-
process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
|
|
4261
|
-
try {
|
|
4262
|
-
execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], {
|
|
4263
|
-
stdio: "inherit",
|
|
4264
|
-
});
|
|
4265
|
-
}
|
|
4266
|
-
catch (err) {
|
|
4267
|
-
logCliOperationFailure(err);
|
|
4268
|
-
throw new MadmaxDetachedReuseError(`refusing duplicate madmax detached launch: existing session ${activeRecord.tmux_session_name} is active but attach failed`);
|
|
4269
|
-
}
|
|
4270
|
-
return { postLaunchHandledExternally: true };
|
|
4271
|
-
}
|
|
4272
|
-
if (activeRecordPath && activeRecord) {
|
|
4273
|
-
rmSync(activeRecordPath, { force: true });
|
|
4274
|
-
}
|
|
4275
|
-
let detachedSessionBindingWrite = Promise.resolve();
|
|
4276
|
-
const writeDetachedSessionBinding = (tmuxPaneId) => {
|
|
4277
|
-
detachedSessionBindingWrite = detachedSessionBindingWrite
|
|
4278
|
-
.catch((err) => {
|
|
4279
|
-
logCliOperationFailure(err);
|
|
4280
|
-
})
|
|
4281
|
-
.then(() => writeSessionStart(cwd, sessionId, {
|
|
4282
|
-
tmuxSessionName: sessionName,
|
|
4283
|
-
...(tmuxPaneId ? { tmuxPaneId } : {}),
|
|
4284
|
-
}));
|
|
4285
|
-
void detachedSessionBindingWrite.catch((err) => {
|
|
4286
|
-
logCliOperationFailure(err);
|
|
4287
|
-
// Non-fatal: managed tmux recovery can still use compatibility fallback.
|
|
4288
|
-
});
|
|
4289
|
-
};
|
|
4290
|
-
writeDetachedSessionBinding();
|
|
4291
|
-
let createdDetachedSession = false;
|
|
4292
|
-
let registeredHookTarget = null;
|
|
4293
|
-
let registeredHookName = null;
|
|
4294
|
-
let registeredClientAttachedHookName = null;
|
|
4295
|
-
let detachedParentEnvFilePath;
|
|
4296
|
-
let detachedLeaderPaneId = null;
|
|
4297
|
-
try {
|
|
4298
|
-
// This path is the user-shell interactive launch: OMX creates a tmux
|
|
4299
|
-
// session and immediately attaches the user's terminal to it. If a tmux
|
|
4300
|
-
// server already exists, `new-session -e` only forwards explicit values,
|
|
4301
|
-
// so provider-specific parent-shell keys would disappear. Source a
|
|
4302
|
-
// private env file inside the leader shell instead of putting every
|
|
4303
|
-
// parent env value on the tmux command line or in logs.
|
|
4304
|
-
if (!nativeWindows) {
|
|
5038
|
+
const contextKey = detachedControlPlane?.values.OMX_MADMAX_DETACHED_CONTEXT.trim() || undefined;
|
|
5039
|
+
const runsRoot = detachedControlPlane?.values.OMX_RUNS_DIR || resolveMadmaxRunsRoot(process.env);
|
|
5040
|
+
const detachedLaunchNonce = randomUUID();
|
|
5041
|
+
const releaseMarkerPath = join(omxRoot(cwd), "runtime", "detached-release", `${sessionId}.${detachedLaunchNonce}.release`);
|
|
5042
|
+
let detachedParentEnvFilePath;
|
|
5043
|
+
let detachedLeaderPaneId = null;
|
|
5044
|
+
let detachedLeaderAuthority = null;
|
|
5045
|
+
let detachedHudAuthority = null;
|
|
5046
|
+
let detachedLeaderPid = null;
|
|
5047
|
+
let rollbackFromPreReportAuthority = false;
|
|
5048
|
+
let attachStep = null;
|
|
5049
|
+
const launchDetachedSession = async () => {
|
|
5050
|
+
let createdSession = false;
|
|
5051
|
+
const bootstrapLeader = async () => {
|
|
5052
|
+
if (!nativeWindows)
|
|
4305
5053
|
detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
|
|
4306
|
-
|
|
4307
|
-
|
|
5054
|
+
const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, nativeWindows && detachedWindowsCodexCmd ? detachedWindowsCodexCmd : codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, codexEnvWithNotify, sqliteHomeOverride, detachedParentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin, {
|
|
5055
|
+
...(preLaunchOptions.notifyTempContract.active ? { notifyTempContract: preLaunchOptions.notifyTempContract } : {}),
|
|
5056
|
+
enableNotifyFallbackAuthority: preLaunchOptions.enableNotifyFallbackAuthority,
|
|
5057
|
+
worktreeDirty: preLaunchOptions.worktreeDirty,
|
|
5058
|
+
shouldAttach: detachedPreflight.shouldAttach,
|
|
5059
|
+
}, detachedControlPlane);
|
|
4308
5060
|
for (const step of bootstrapSteps) {
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
if (leaderPaneId) {
|
|
5061
|
+
try {
|
|
5062
|
+
if (step.name === "new-session") {
|
|
5063
|
+
const output = execTmuxFileSync(step.args, { stdio: "pipe", encoding: "utf-8" });
|
|
5064
|
+
createdSession = true;
|
|
5065
|
+
const leaderPaneId = parsePaneIdFromTmuxOutput(output || "");
|
|
5066
|
+
if (!leaderPaneId)
|
|
5067
|
+
throw new Error("detached session did not report a leader pane id");
|
|
4317
5068
|
detachedLeaderPaneId = leaderPaneId;
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
writeMadmaxDetachedActiveRecord(activeRecordPath, {
|
|
4321
|
-
version: 1,
|
|
4322
|
-
context_key: contextKey,
|
|
4323
|
-
created_at: new Date().toISOString(),
|
|
4324
|
-
source_cwd: runtimeContext?.sourceCwd ?? process.env.OMX_SOURCE_CWD ?? cwd,
|
|
4325
|
-
...(runtimeContext?.worktreeCwd ? { worktree_cwd: runtimeContext.worktreeCwd } : {}),
|
|
4326
|
-
argv: args,
|
|
4327
|
-
run_dir: runtimeContext?.omxRoot ?? process.env.OMX_ROOT ?? cwd,
|
|
4328
|
-
tmux_session_name: sessionName,
|
|
4329
|
-
session_id: sessionId,
|
|
4330
|
-
tmux_pane_id: leaderPaneId,
|
|
4331
|
-
});
|
|
4332
|
-
}
|
|
4333
|
-
writeDetachedSessionBinding(leaderPaneId);
|
|
5069
|
+
detachedLeaderAuthority = captureDetachedLeaderAuthority(leaderPaneId, sessionName, sessionId);
|
|
5070
|
+
continue;
|
|
4334
5071
|
}
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
:
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
if (nativeWindows && detachedWindowsCodexCmd) {
|
|
4352
|
-
scheduleDetachedWindowsCodexLaunch(sessionName, detachedWindowsCodexCmd);
|
|
5072
|
+
const authority = detachedLeaderAuthority;
|
|
5073
|
+
if (!authority)
|
|
5074
|
+
throw new Error("detached leader authority missing before tmux mutation");
|
|
5075
|
+
if (step.name === "tag-session") {
|
|
5076
|
+
runDetachedLeaderMutation(authority, step.args, false);
|
|
5077
|
+
// tmux can acknowledge the session tag before the first owner-format
|
|
5078
|
+
// evaluation sees it. Retrying this idempotent first owner-guarded
|
|
5079
|
+
// mutation once gives the server a command boundary without relaxing
|
|
5080
|
+
// any part of the captured session, window, pane, PID, or owner fence.
|
|
5081
|
+
runDetachedLeaderMutation(authority, ["set-option", "-q", "-t", authority.sessionName, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)], true, true);
|
|
5082
|
+
runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)]);
|
|
5083
|
+
// #3266: the owned leader pane must close with its process so a normal
|
|
5084
|
+
// child exit destroys the session naturally even when the user's tmux
|
|
5085
|
+
// config inherits remain-on-exit=on/failed.
|
|
5086
|
+
runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "remain-on-exit", "off"]);
|
|
5087
|
+
continue;
|
|
4353
5088
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
5089
|
+
if (step.name !== "split-and-capture-hud-pane") {
|
|
5090
|
+
throw new Error(`unexpected detached bootstrap mutation: ${step.name}`);
|
|
5091
|
+
}
|
|
5092
|
+
detachedHudAuthority = runDetachedLeaderSplit(authority, step.args);
|
|
5093
|
+
tagDetachedHudPane(authority, detachedHudAuthority, sessionId);
|
|
5094
|
+
for (const finalizeStep of buildDetachedSessionFinalizeSteps(authority.sessionName, detachedHudAuthority.paneId, authority.windowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, detachedPreflight.shouldAttach, authority.paneId)) {
|
|
5095
|
+
if (finalizeStep.name === "attach-session") {
|
|
5096
|
+
attachStep = finalizeStep;
|
|
4362
5097
|
continue;
|
|
4363
5098
|
}
|
|
4364
|
-
|
|
4365
|
-
try {
|
|
4366
|
-
const startedAtMs = Date.now();
|
|
4367
|
-
execTmuxFileSync(finalizeStep.args, { stdio });
|
|
4368
|
-
if (finalizeStep.name === "attach-session") {
|
|
4369
|
-
assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
|
|
4370
|
-
}
|
|
4371
|
-
}
|
|
4372
|
-
catch (err) {
|
|
4373
|
-
logCliOperationFailure(err);
|
|
4374
|
-
if (finalizeStep.name === "attach-session")
|
|
4375
|
-
throw new Error("failed to attach detached tmux session");
|
|
5099
|
+
if (finalizeStep.name === "sanitize-copy-mode-style")
|
|
4376
5100
|
continue;
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
}
|
|
4388
|
-
if (finalizeStep.name === "reconcile-hud-resize") {
|
|
4389
|
-
registerDetachedHudLayoutReconcileHook({
|
|
4390
|
-
hudPaneId,
|
|
4391
|
-
detachedLeaderPaneId,
|
|
4392
|
-
cwd,
|
|
4393
|
-
sessionId,
|
|
4394
|
-
omxBin,
|
|
4395
|
-
omxRootOverride,
|
|
4396
|
-
baseEnv: runtimeHookEnv,
|
|
4397
|
-
});
|
|
4398
|
-
}
|
|
5101
|
+
const targetsHudPane = new Set([
|
|
5102
|
+
"register-resize-hook",
|
|
5103
|
+
"register-client-attached-reconcile",
|
|
5104
|
+
"schedule-delayed-resize",
|
|
5105
|
+
"reconcile-hud-resize",
|
|
5106
|
+
]).has(finalizeStep.name);
|
|
5107
|
+
if (targetsHudPane)
|
|
5108
|
+
runDetachedHudMutation(authority, detachedHudAuthority, guardDetachedHudDeferredMutation(authority, detachedHudAuthority, finalizeStep.args));
|
|
5109
|
+
else
|
|
5110
|
+
runDetachedLeaderMutation(authority, finalizeStep.args);
|
|
4399
5111
|
}
|
|
4400
5112
|
}
|
|
5113
|
+
catch (error) {
|
|
5114
|
+
throw new DetachedLaunchSafetyError(step.name === "new-session" ? "inert-session" : "pane-id", error, detachedPreflight.report);
|
|
5115
|
+
}
|
|
4401
5116
|
}
|
|
4402
|
-
return
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
5117
|
+
return undefined;
|
|
5118
|
+
};
|
|
5119
|
+
const launchTerminal = await executeDetachedLaunchStateMachine({ preflight: detachedPreflight }, {
|
|
5120
|
+
establish: bootstrapLeader,
|
|
5121
|
+
complete: async () => {
|
|
5122
|
+
const readyDeadline = Date.now() + DETACHED_LEADER_READY_TIMEOUT_MS;
|
|
5123
|
+
while (true) {
|
|
5124
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5125
|
+
if (isDetachedReadyReportAuthorized(report, {
|
|
5126
|
+
nonce: detachedLaunchNonce,
|
|
5127
|
+
sessionId,
|
|
5128
|
+
sessionName,
|
|
5129
|
+
shouldAttach: detachedPreflight.shouldAttach,
|
|
5130
|
+
leaderPaneId: detachedLeaderPaneId,
|
|
5131
|
+
leaderPanePid: detachedLeaderAuthority?.panePid ?? null,
|
|
5132
|
+
})) {
|
|
5133
|
+
detachedLeaderPid = report.leaderPid;
|
|
5134
|
+
return { kind: "success" };
|
|
5135
|
+
}
|
|
5136
|
+
if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId && report.sessionName === sessionName && report.leaderPid && report.kind === "failed") {
|
|
5137
|
+
detachedLeaderPid = report.leaderPid;
|
|
5138
|
+
return { kind: "failure", operation: "session-instructions", error: new Error(report.error || "detached leader setup failed") };
|
|
5139
|
+
}
|
|
5140
|
+
if (Date.now() >= readyDeadline)
|
|
5141
|
+
return { kind: "failure", operation: "session-instructions", error: new Error("detached leader readiness timed out") };
|
|
5142
|
+
blockMs(20);
|
|
5143
|
+
}
|
|
5144
|
+
},
|
|
5145
|
+
createInertSession: async () => {
|
|
5146
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5147
|
+
if (report?.kind !== "ready" || report.nonce !== detachedLaunchNonce || report.sessionId !== sessionId || report.sessionName !== sessionName || report.leaderPid !== detachedLeaderPid) {
|
|
5148
|
+
throw new Error("detached ready report does not bind the inert session");
|
|
5149
|
+
}
|
|
5150
|
+
return sessionName;
|
|
5151
|
+
},
|
|
5152
|
+
capturePane: async () => {
|
|
5153
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5154
|
+
if (!detachedLeaderPaneId || report?.kind !== "ready" || report.paneId !== detachedLeaderPaneId || report.leaderPid !== detachedLeaderPid) {
|
|
5155
|
+
throw new Error("detached ready report does not bind the leader pane");
|
|
5156
|
+
}
|
|
5157
|
+
return detachedLeaderPaneId;
|
|
5158
|
+
},
|
|
5159
|
+
updateNameMetadata: async () => {
|
|
5160
|
+
const state = await readSessionState(cwd, detachedSelectedStateEnv);
|
|
5161
|
+
if (state?.session_id !== sessionId || state.tmux_session_name !== sessionName) {
|
|
5162
|
+
throw new Error("detached session-name metadata was not committed by the leader");
|
|
5163
|
+
}
|
|
5164
|
+
return "committed-released";
|
|
5165
|
+
},
|
|
5166
|
+
updatePaneMetadata: async (_binding, pane) => {
|
|
5167
|
+
const state = await readSessionState(cwd, detachedSelectedStateEnv);
|
|
5168
|
+
if (state?.session_id !== sessionId || state.tmux_pane_id !== pane) {
|
|
5169
|
+
throw new Error("detached pane metadata was not committed by the leader");
|
|
5170
|
+
}
|
|
5171
|
+
return "committed-released";
|
|
5172
|
+
},
|
|
5173
|
+
publishActiveRecord: async () => {
|
|
5174
|
+
const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(runsRoot, contextKey) : join(omxRoot(cwd), "state", "detached-active-record.json");
|
|
5175
|
+
const record = readMadmaxDetachedActiveRecord(activeRecordPath);
|
|
5176
|
+
if (!record || record.launch_nonce !== detachedLaunchNonce || record.leader_pid !== detachedLeaderPid || record.session_id !== sessionId || record.tmux_session_name !== sessionName || record.tmux_pane_id !== detachedLeaderPaneId)
|
|
5177
|
+
throw new Error("detached active record does not bind the ready leader");
|
|
5178
|
+
const bytes = readFileSync(activeRecordPath, "utf-8");
|
|
5179
|
+
return { bytes, digest: createHash("sha256").update(bytes).digest("hex"), nonce: detachedLaunchNonce };
|
|
5180
|
+
},
|
|
5181
|
+
finalizeSetupFailure: async () => { },
|
|
5182
|
+
releaseBarrier: async () => {
|
|
5183
|
+
if (!detachedLeaderPid)
|
|
5184
|
+
throw new Error("detached leader PID missing before release");
|
|
5185
|
+
publishDetachedReleaseMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid, detachedHudAuthority ?? undefined);
|
|
5186
|
+
},
|
|
5187
|
+
abortAndAwaitFinalization: async () => {
|
|
5188
|
+
// The leader has the retained binding. Publishing abort is the only
|
|
5189
|
+
// outer action permitted after a D9 write failure; a mismatched or
|
|
5190
|
+
// missing report leaves every artifact and the tmux session intact.
|
|
5191
|
+
if (!detachedLeaderPid) {
|
|
5192
|
+
rollbackFromPreReportAuthority = detachedLeaderAuthority !== null;
|
|
5193
|
+
return { acknowledged: false, rollbackAuthorized: rollbackFromPreReportAuthority };
|
|
5194
|
+
}
|
|
5195
|
+
const current = readDetachedLeaderReport(releaseMarkerPath);
|
|
5196
|
+
if (current?.nonce === detachedLaunchNonce && current.sessionId === sessionId &&
|
|
5197
|
+
current.sessionName === sessionName && current.leaderPid === detachedLeaderPid &&
|
|
5198
|
+
current.finalized === true && (current.kind === "failed" || current.kind === "terminal"))
|
|
5199
|
+
return {
|
|
5200
|
+
acknowledged: true,
|
|
5201
|
+
nonce: current.nonce,
|
|
5202
|
+
sessionId: current.sessionId,
|
|
5203
|
+
sessionName: current.sessionName,
|
|
5204
|
+
leaderPid: current.leaderPid,
|
|
5205
|
+
kind: current.kind,
|
|
5206
|
+
};
|
|
5207
|
+
publishDetachedAbortMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid);
|
|
5208
|
+
const deadline = Date.now() + 30_000;
|
|
5209
|
+
while (Date.now() < deadline) {
|
|
5210
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5211
|
+
if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId &&
|
|
5212
|
+
report.sessionName === sessionName && report.leaderPid === detachedLeaderPid &&
|
|
5213
|
+
report.finalized === true && (report.kind === "failed" || report.kind === "terminal"))
|
|
5214
|
+
return {
|
|
5215
|
+
acknowledged: true,
|
|
5216
|
+
nonce: report.nonce,
|
|
5217
|
+
sessionId: report.sessionId,
|
|
5218
|
+
sessionName: report.sessionName,
|
|
5219
|
+
leaderPid: report.leaderPid,
|
|
5220
|
+
kind: report.kind,
|
|
5221
|
+
};
|
|
5222
|
+
blockMs(20);
|
|
5223
|
+
}
|
|
5224
|
+
return { acknowledged: false };
|
|
5225
|
+
},
|
|
5226
|
+
attachOrReturn: async () => { if (attachStep)
|
|
5227
|
+
execTmuxFileSync(attachStep.args, { stdio: "inherit" }); },
|
|
5228
|
+
rollback: async (_ownedRecord, report) => {
|
|
5229
|
+
const attempt = async (step, operation) => {
|
|
5230
|
+
report.rollback.attempted.push(step);
|
|
4414
5231
|
try {
|
|
4415
|
-
|
|
5232
|
+
await operation();
|
|
5233
|
+
}
|
|
5234
|
+
catch (error) {
|
|
5235
|
+
report.rollback.failures.push({ step, status: "failed", code: detachedFailureCode(error) });
|
|
4416
5236
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
5237
|
+
};
|
|
5238
|
+
await attempt("parent-env", async () => { if (detachedParentEnvFilePath)
|
|
5239
|
+
await rm(detachedParentEnvFilePath, { force: true }); });
|
|
5240
|
+
await attempt("runtime-codex-home", () => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
|
|
5241
|
+
await attempt("rollback", () => { rmSync(releaseMarkerPath, { force: true }); rmSync(`${releaseMarkerPath}.release`, { force: true }); rmSync(`${releaseMarkerPath}.abort`, { force: true }); });
|
|
5242
|
+
if (createdSession) {
|
|
5243
|
+
for (const step of buildDetachedSessionRollbackSteps(sessionName, null, null, null)) {
|
|
5244
|
+
await attempt(`session:${step.name}`, () => {
|
|
5245
|
+
if (!detachedLeaderAuthority)
|
|
5246
|
+
throw new Error("detached leader authority missing before rollback");
|
|
5247
|
+
if (rollbackFromPreReportAuthority && step.name === "kill-session") {
|
|
5248
|
+
cleanupDetachedPreReportSession(detachedLeaderAuthority);
|
|
5249
|
+
return;
|
|
5250
|
+
}
|
|
5251
|
+
runDetachedLeaderMutation(detachedLeaderAuthority, step.args);
|
|
5252
|
+
});
|
|
4420
5253
|
}
|
|
4421
5254
|
}
|
|
5255
|
+
},
|
|
5256
|
+
});
|
|
5257
|
+
if (launchTerminal.kind === "attached" && detachedLeaderPid) {
|
|
5258
|
+
// #3266: propagate the finalized detached child's exit status to the invoking shell.
|
|
5259
|
+
// A rejected status protocol is only benign when the exact owned session
|
|
5260
|
+
// provably survives (manual detach); a destroyed or ambiguous session with
|
|
5261
|
+
// no authenticated finalized status is a protocol failure, never silent success.
|
|
5262
|
+
const attachResolution = resolveDetachedAttachExitStatus(releaseMarkerPath, { nonce: detachedLaunchNonce, sessionId, sessionName, leaderPid: detachedLeaderPid }, { exactSessionExists: () => probeExactDetachedSessionExists(detachedLeaderAuthority) });
|
|
5263
|
+
if (attachResolution === "protocol-failure") {
|
|
5264
|
+
process.stderr.write("[omx] detached session ended without an authenticated finalized terminal status; treating the launch as failed.\n");
|
|
5265
|
+
if (!process.exitCode)
|
|
5266
|
+
process.exitCode = 1;
|
|
4422
5267
|
}
|
|
4423
|
-
throw err;
|
|
4424
5268
|
}
|
|
5269
|
+
return { postLaunchHandledExternally: true };
|
|
4425
5270
|
};
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
try {
|
|
4429
|
-
if (isMadmaxDetachedGuardEnabled(process.env) && contextKey) {
|
|
4430
|
-
return withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
|
|
4431
|
-
}
|
|
4432
|
-
return launchDetachedSession();
|
|
4433
|
-
}
|
|
4434
|
-
catch (err) {
|
|
4435
|
-
if (err instanceof MadmaxDetachedReuseError || err instanceof MadmaxDetachedGuardError) {
|
|
4436
|
-
throw err;
|
|
4437
|
-
}
|
|
4438
|
-
logCliOperationFailure(err);
|
|
4439
|
-
// tmux not available or failed, just run codex directly
|
|
4440
|
-
runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
|
|
4441
|
-
return { postLaunchHandledExternally: false };
|
|
5271
|
+
if (contextKey) {
|
|
5272
|
+
return await withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
|
|
4442
5273
|
}
|
|
5274
|
+
return await launchDetachedSession();
|
|
4443
5275
|
}
|
|
4444
5276
|
}
|
|
4445
5277
|
function listHudWatchPaneIdsInCurrentWindow(currentPaneId, owner = {}) {
|
|
@@ -4498,6 +5330,24 @@ export function buildWindowsPromptCommand(command, args) {
|
|
|
4498
5330
|
].join("; ");
|
|
4499
5331
|
return `powershell.exe -NoLogo -NoExit -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
|
|
4500
5332
|
}
|
|
5333
|
+
/**
|
|
5334
|
+
* Build a Windows child command for detached ownership. Unlike the interactive
|
|
5335
|
+
* prompt wrapper, this process exits as soon as Codex does so the outer barrier
|
|
5336
|
+
* can run its post-launch cleanup.
|
|
5337
|
+
*/
|
|
5338
|
+
export function buildWindowsDetachedChildCommand(command, args) {
|
|
5339
|
+
const invocation = [
|
|
5340
|
+
"&",
|
|
5341
|
+
quotePowerShellArg(command),
|
|
5342
|
+
...args.map(quotePowerShellArg),
|
|
5343
|
+
].join(" ");
|
|
5344
|
+
const wrappedCommand = [
|
|
5345
|
+
"$ErrorActionPreference = 'Stop'",
|
|
5346
|
+
invocation,
|
|
5347
|
+
"exit $LASTEXITCODE",
|
|
5348
|
+
].join("; ");
|
|
5349
|
+
return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
|
|
5350
|
+
}
|
|
4501
5351
|
/**
|
|
4502
5352
|
* Wrap a command for tmux pane execution while preserving the tmux pane cwd.
|
|
4503
5353
|
* tmux already starts the pane at `-c <cwd>`; using a login shell here can
|
|
@@ -4534,45 +5384,323 @@ function quoteShellArg(value) {
|
|
|
4534
5384
|
function quotePowerShellArg(value) {
|
|
4535
5385
|
return `'${value.replace(/'/g, "''")}'`;
|
|
4536
5386
|
}
|
|
4537
|
-
export function
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
5387
|
+
export function decodeDetachedLeaderPayload(encoded) {
|
|
5388
|
+
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded))
|
|
5389
|
+
throw new Error("invalid detached leader payload");
|
|
5390
|
+
let value;
|
|
5391
|
+
try {
|
|
5392
|
+
value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
5393
|
+
}
|
|
5394
|
+
catch {
|
|
5395
|
+
throw new Error("invalid detached leader payload");
|
|
5396
|
+
}
|
|
5397
|
+
if (!value || typeof value !== "object")
|
|
5398
|
+
throw new Error("invalid detached leader payload");
|
|
5399
|
+
const payload = value;
|
|
5400
|
+
const options = payload.preLaunchOptions;
|
|
5401
|
+
if (typeof payload.cwd !== "string" || typeof payload.sessionName !== "string" || typeof payload.sessionId !== "string" ||
|
|
5402
|
+
typeof payload.codexCmd !== "string" || !options || typeof options !== "object" ||
|
|
5403
|
+
typeof options.enableNotifyFallbackAuthority !== "boolean" ||
|
|
5404
|
+
typeof options.worktreeDirty !== "boolean" ||
|
|
5405
|
+
typeof options.shouldAttach !== "boolean")
|
|
5406
|
+
throw new Error("invalid detached leader payload");
|
|
5407
|
+
const notifyTempContract = options.notifyTempContract;
|
|
5408
|
+
if (notifyTempContract !== undefined && (!notifyTempContract || typeof notifyTempContract !== "object"))
|
|
5409
|
+
throw new Error("invalid detached leader payload");
|
|
5410
|
+
const parentEnv = payload.parentEnv;
|
|
5411
|
+
if (parentEnv !== undefined && (!parentEnv || typeof parentEnv !== "object" ||
|
|
5412
|
+
Object.entries(parentEnv).some(([key, value]) => !SHELL_ENV_NAME_PATTERN.test(key) || DETACHED_SESSION_PANE_ENV_KEYS.has(key) || typeof value !== "string" || value.includes("\0")))) {
|
|
5413
|
+
throw new Error("invalid detached leader parent environment");
|
|
5414
|
+
}
|
|
5415
|
+
return {
|
|
5416
|
+
cwd: payload.cwd, sessionName: payload.sessionName, sessionId: payload.sessionId, codexCmd: payload.codexCmd,
|
|
5417
|
+
...(typeof payload.codexHomeOverride === "string" ? { codexHomeOverride: payload.codexHomeOverride } : {}),
|
|
5418
|
+
...(typeof payload.projectLocalCodexHomeForCleanup === "string" ? { projectLocalCodexHomeForCleanup: payload.projectLocalCodexHomeForCleanup } : {}),
|
|
5419
|
+
...(typeof payload.runtimeCodexHomeForCleanup === "string" ? { runtimeCodexHomeForCleanup: payload.runtimeCodexHomeForCleanup } : {}),
|
|
5420
|
+
...(parentEnv ? { parentEnv: parentEnv } : {}),
|
|
5421
|
+
...(typeof payload.readyPath === "string" ? { readyPath: payload.readyPath } : {}),
|
|
5422
|
+
preLaunchOptions: {
|
|
5423
|
+
...(notifyTempContract ? { notifyTempContract: notifyTempContract } : {}),
|
|
5424
|
+
enableNotifyFallbackAuthority: options.enableNotifyFallbackAuthority,
|
|
5425
|
+
worktreeDirty: options.worktreeDirty,
|
|
5426
|
+
shouldAttach: options.shouldAttach,
|
|
5427
|
+
},
|
|
5428
|
+
};
|
|
4552
5429
|
}
|
|
4553
|
-
function
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
5430
|
+
function parseDetachedHudAuthorityProof(value) {
|
|
5431
|
+
if (!value || typeof value !== "object")
|
|
5432
|
+
return undefined;
|
|
5433
|
+
const proof = value;
|
|
5434
|
+
if (typeof proof.paneId !== "string" || !/^%[0-9]+$/.test(proof.paneId))
|
|
5435
|
+
return undefined;
|
|
5436
|
+
if (typeof proof.panePid !== "number" || !Number.isSafeInteger(proof.panePid) || proof.panePid <= 0)
|
|
5437
|
+
return undefined;
|
|
5438
|
+
if (typeof proof.sessionId !== "string" || !/^\$[0-9]+$/.test(proof.sessionId))
|
|
5439
|
+
return undefined;
|
|
5440
|
+
if (typeof proof.windowId !== "string" || !/^@[0-9]+$/.test(proof.windowId))
|
|
5441
|
+
return undefined;
|
|
5442
|
+
if (typeof proof.operationMarker !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(proof.operationMarker))
|
|
5443
|
+
return undefined;
|
|
5444
|
+
return { paneId: proof.paneId, panePid: proof.panePid, sessionId: proof.sessionId, windowId: proof.windowId, operationMarker: proof.operationMarker };
|
|
5445
|
+
}
|
|
5446
|
+
function teardownDetachedOwnedHudPane(leaderPaneId, payload, hudProof) {
|
|
5447
|
+
// #3266: on a normal child exit, kill exactly the bootstrap-proven OMX HUD pane so
|
|
5448
|
+
// the leader pane becomes the last live pane and the owned session closes naturally,
|
|
5449
|
+
// returning the attached client to its shell. Fail-closed: any doubt means zero mutations.
|
|
5450
|
+
// Contract: only the proven HUD is removed. A pane the user added to the detached
|
|
5451
|
+
// session is deliberately preserved, which intentionally keeps that session (and any
|
|
5452
|
+
// attached client) alive; natural closure and shell return happen only when no other
|
|
5453
|
+
// panes remain.
|
|
5454
|
+
const discovered = discoverDetachedHudAuthority(payload.sessionName, payload.sessionId);
|
|
5455
|
+
const proof = discovered ?? hudProof;
|
|
5456
|
+
if (!proof)
|
|
5457
|
+
return;
|
|
5458
|
+
try {
|
|
5459
|
+
cleanupDetachedHudPane(proof, discovered ? payload.sessionId : undefined);
|
|
5460
|
+
execTmuxFileSync(["kill-pane", "-t", leaderPaneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5461
|
+
}
|
|
5462
|
+
catch (error) {
|
|
5463
|
+
traceDetachedLeaderPhase(`hud-teardown-error:${describeDetachedLeaderFailure(error)}`);
|
|
5464
|
+
logCliOperationFailure(error);
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
function traceDetachedLeaderPhase(phase) {
|
|
5468
|
+
if (process.env.OMX_TEST_DETACHED_TRACE !== "1")
|
|
5469
|
+
return;
|
|
5470
|
+
const line = `[omx-test-detached] ${phase}\n`;
|
|
5471
|
+
process.stderr.write(line);
|
|
5472
|
+
const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
|
|
5473
|
+
if (tracePath)
|
|
5474
|
+
appendFileSync(tracePath, line);
|
|
5475
|
+
}
|
|
5476
|
+
async function runDetachedSessionLeader(payload) {
|
|
5477
|
+
for (const [key, value] of Object.entries(payload.parentEnv ?? {})) {
|
|
5478
|
+
if (isDetachedLaunchControlPlaneKey(key, process.platform === "win32"))
|
|
5479
|
+
continue;
|
|
5480
|
+
process.env[key] = value;
|
|
5481
|
+
}
|
|
5482
|
+
let pane;
|
|
5483
|
+
const inheritedPane = process.env.TMUX_PANE?.trim();
|
|
5484
|
+
if (payload.preLaunchOptions.shouldAttach === false) {
|
|
5485
|
+
if (!inheritedPane || !/^%[0-9]+$/.test(inheritedPane)) {
|
|
5486
|
+
throw new Error("detached Hermes leader has no tmux pane identity");
|
|
5487
|
+
}
|
|
5488
|
+
pane = inheritedPane;
|
|
5489
|
+
}
|
|
5490
|
+
else {
|
|
5491
|
+
const paneSnapshot = execTmuxFileSync(["list-panes", "-t", payload.sessionName, "-F", "#{pane_id}\t#{pane_dead}\t#{pane_pid}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5492
|
+
pane = parseDetachedLeaderPaneIdByPid(paneSnapshot, process.pid);
|
|
5493
|
+
}
|
|
5494
|
+
process.env.TMUX_PANE = pane;
|
|
5495
|
+
const established = await establishPreLaunchBinding(payload.cwd, payload.sessionId, {
|
|
5496
|
+
tmuxSessionName: payload.sessionName,
|
|
5497
|
+
tmuxPaneId: pane,
|
|
4558
5498
|
});
|
|
4559
|
-
|
|
5499
|
+
const binding = established.binding;
|
|
5500
|
+
let ownedRecord;
|
|
5501
|
+
let detachedHudProof;
|
|
5502
|
+
const nonce = payload.readyPath?.split(".").at(-2) || payload.sessionId;
|
|
5503
|
+
const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
|
|
5504
|
+
const activeRecordPath = contextKey
|
|
5505
|
+
? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey)
|
|
5506
|
+
: join(omxRoot(payload.cwd), "state", "detached-active-record.json");
|
|
5507
|
+
let externalInterrupt;
|
|
5508
|
+
try {
|
|
5509
|
+
const completion = await completePreLaunchSetup(payload.cwd, payload.sessionId, payload.preLaunchOptions.notifyTempContract, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.preLaunchOptions.worktreeDirty);
|
|
5510
|
+
if (completion.kind === "failure") {
|
|
5511
|
+
const finalization = await finalizeBoundOnce(binding, "setup-failure", payload.cwd);
|
|
5512
|
+
if (!finalization.finalized)
|
|
5513
|
+
throw new Error(`bound setup finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
|
|
5514
|
+
throw completion.error;
|
|
5515
|
+
}
|
|
5516
|
+
const record = {
|
|
5517
|
+
version: 1, context_key: contextKey ?? payload.sessionId, created_at: new Date().toISOString(),
|
|
5518
|
+
source_cwd: payload.cwd, argv: [payload.codexCmd], run_dir: process.env.OMX_ROOT ?? payload.cwd,
|
|
5519
|
+
tmux_session_name: payload.sessionName, session_id: payload.sessionId, tmux_pane_id: pane,
|
|
5520
|
+
launch_nonce: nonce, leader_pid: process.pid, base_state_root: binding.context.baseStateDir,
|
|
5521
|
+
lifecycle_phase: "ready",
|
|
5522
|
+
};
|
|
5523
|
+
const runtimeBinding = queryMadmaxDetachedRuntimeBinding(record);
|
|
5524
|
+
ownedRecord = writeMadmaxDetachedActiveRecord(activeRecordPath, {
|
|
5525
|
+
...record,
|
|
5526
|
+
...(runtimeBinding ? {
|
|
5527
|
+
tmux_internal_session_id: runtimeBinding.internalSessionId,
|
|
5528
|
+
tmux_session_created: runtimeBinding.sessionCreated,
|
|
5529
|
+
tmux_pane_pid: runtimeBinding.panePid,
|
|
5530
|
+
} : {}),
|
|
5531
|
+
});
|
|
5532
|
+
if (payload.readyPath) {
|
|
5533
|
+
writeDetachedLeaderReport(payload.readyPath, { version: 1, kind: "ready", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName, paneId: pane, leaderPid: process.pid });
|
|
5534
|
+
const releasePath = `${payload.readyPath}.release`;
|
|
5535
|
+
const abortPath = `${payload.readyPath}.abort`;
|
|
5536
|
+
const releaseDeadline = Date.now() + 30_000;
|
|
5537
|
+
let interrupted;
|
|
5538
|
+
const interrupt = (signal) => { interrupted = signal; };
|
|
5539
|
+
process.once("SIGINT", () => interrupt("SIGINT"));
|
|
5540
|
+
process.once("SIGTERM", () => interrupt("SIGTERM"));
|
|
5541
|
+
process.once("SIGHUP", () => interrupt("SIGHUP"));
|
|
5542
|
+
while (true) {
|
|
5543
|
+
if (interrupted)
|
|
5544
|
+
throw new Error(`detached leader interrupted before release: ${interrupted}`);
|
|
5545
|
+
const release = readDetachedLeaderReport(releasePath);
|
|
5546
|
+
if (release?.kind === "release" && release.nonce === nonce && release.sessionId === payload.sessionId && release.sessionName === payload.sessionName && release.leaderPid === process.pid) {
|
|
5547
|
+
detachedHudProof = parseDetachedHudAuthorityProof(release.hud);
|
|
5548
|
+
break;
|
|
5549
|
+
}
|
|
5550
|
+
const abort = readDetachedLeaderReport(abortPath);
|
|
5551
|
+
if (abort?.kind === "abort" && abort.nonce === nonce && abort.sessionId === payload.sessionId && abort.sessionName === payload.sessionName && abort.leaderPid === process.pid) {
|
|
5552
|
+
throw new Error("detached launch aborted before release");
|
|
5553
|
+
}
|
|
5554
|
+
if (Date.now() >= releaseDeadline)
|
|
5555
|
+
throw new Error("detached leader release timed out");
|
|
5556
|
+
blockMs(20);
|
|
5557
|
+
}
|
|
5558
|
+
rmSync(releasePath, { force: true });
|
|
5559
|
+
rmSync(abortPath, { force: true });
|
|
5560
|
+
}
|
|
5561
|
+
const child = spawn(process.platform === "win32" ? "powershell.exe" : "/bin/sh", process.platform === "win32" ? ["-NoProfile", "-Command", payload.codexCmd] : ["-c", payload.codexCmd], {
|
|
5562
|
+
cwd: payload.cwd,
|
|
5563
|
+
stdio: "inherit",
|
|
5564
|
+
...(process.platform === "win32" ? {} : { detached: true }),
|
|
5565
|
+
});
|
|
5566
|
+
let escalation;
|
|
5567
|
+
const signalChildTree = (signal, force = false) => {
|
|
5568
|
+
if (!child.pid)
|
|
5569
|
+
return;
|
|
5570
|
+
try {
|
|
5571
|
+
if (process.platform === "win32")
|
|
5572
|
+
execFileSync("taskkill", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], { stdio: "ignore" });
|
|
5573
|
+
else
|
|
5574
|
+
process.kill(-child.pid, signal);
|
|
5575
|
+
}
|
|
5576
|
+
catch { }
|
|
5577
|
+
};
|
|
5578
|
+
const forward = (signal) => {
|
|
5579
|
+
externalInterrupt ??= signal;
|
|
5580
|
+
signalChildTree(signal);
|
|
5581
|
+
if (!escalation) {
|
|
5582
|
+
escalation = setTimeout(() => signalChildTree("SIGKILL", true), 5_000);
|
|
5583
|
+
escalation.unref();
|
|
5584
|
+
}
|
|
5585
|
+
};
|
|
5586
|
+
process.once("SIGINT", () => forward("SIGINT"));
|
|
5587
|
+
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
5588
|
+
process.once("SIGHUP", () => forward("SIGHUP"));
|
|
5589
|
+
traceDetachedLeaderPhase("child-wait-start");
|
|
5590
|
+
const outcome = await new Promise((resolveChild, rejectChild) => {
|
|
5591
|
+
child.once("error", rejectChild);
|
|
5592
|
+
child.once("exit", (code, signal) => resolveChild({ code, signal }));
|
|
5593
|
+
});
|
|
5594
|
+
traceDetachedLeaderPhase(`child-exit:${String(outcome.code)}:${String(outcome.signal)}`);
|
|
5595
|
+
if (escalation)
|
|
5596
|
+
clearTimeout(escalation);
|
|
5597
|
+
if (process.platform !== "win32" && child.pid) {
|
|
5598
|
+
const waitForGroupExit = async (deadline) => {
|
|
5599
|
+
while (Date.now() < deadline) {
|
|
5600
|
+
try {
|
|
5601
|
+
process.kill(-child.pid, 0);
|
|
5602
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 20));
|
|
5603
|
+
}
|
|
5604
|
+
catch {
|
|
5605
|
+
return true;
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
return false;
|
|
5609
|
+
};
|
|
5610
|
+
let groupGone = await waitForGroupExit(Date.now() + 5_000);
|
|
5611
|
+
if (!groupGone) {
|
|
5612
|
+
signalChildTree("SIGKILL", true);
|
|
5613
|
+
groupGone = await waitForGroupExit(Date.now() + 5_000);
|
|
5614
|
+
}
|
|
5615
|
+
if (!groupGone)
|
|
5616
|
+
throw new Error("detached child process group did not disappear after forced termination");
|
|
5617
|
+
}
|
|
5618
|
+
const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143, SIGKILL: 137 };
|
|
5619
|
+
process.exitCode = externalInterrupt
|
|
5620
|
+
? signalExitCodes[externalInterrupt] ?? 1
|
|
5621
|
+
: outcome.code ?? (outcome.signal ? signalExitCodes[outcome.signal] ?? 1 : 1);
|
|
5622
|
+
traceDetachedLeaderPhase("post-launch-start");
|
|
5623
|
+
await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup, async () => {
|
|
5624
|
+
if (payload.readyPath)
|
|
5625
|
+
writeDetachedLeaderReport(payload.readyPath, {
|
|
5626
|
+
version: 1, kind: "terminal", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
|
|
5627
|
+
paneId: pane, leaderPid: process.pid, finalized: true,
|
|
5628
|
+
...(typeof process.exitCode === "number" ? { exitStatus: process.exitCode } : {}),
|
|
5629
|
+
});
|
|
5630
|
+
traceDetachedLeaderPhase("terminal-report-written");
|
|
5631
|
+
if (ownedRecord) {
|
|
5632
|
+
const bytes = readFileSync(activeRecordPath, "utf-8");
|
|
5633
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
5634
|
+
if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
|
|
5635
|
+
rmSync(activeRecordPath, { force: true });
|
|
5636
|
+
}
|
|
5637
|
+
if (!externalInterrupt && outcome.code !== null) {
|
|
5638
|
+
traceDetachedLeaderPhase("hud-teardown-start");
|
|
5639
|
+
teardownDetachedOwnedHudPane(pane, payload, detachedHudProof);
|
|
5640
|
+
traceDetachedLeaderPhase("hud-teardown-done");
|
|
5641
|
+
}
|
|
5642
|
+
});
|
|
5643
|
+
traceDetachedLeaderPhase("post-launch-done");
|
|
5644
|
+
await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
|
|
5645
|
+
traceDetachedLeaderPhase("leader-return");
|
|
5646
|
+
}
|
|
5647
|
+
catch (error) {
|
|
5648
|
+
let failure = error;
|
|
5649
|
+
let finalized = false;
|
|
5650
|
+
try {
|
|
5651
|
+
await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup);
|
|
5652
|
+
finalized = true;
|
|
5653
|
+
await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
|
|
5654
|
+
}
|
|
5655
|
+
catch (lifecycleError) {
|
|
5656
|
+
failure = new AggregateError([error, lifecycleError], `detached leader failure finalization did not complete: launch=${describeDetachedLeaderFailure(error)}; lifecycle=${describeDetachedLeaderFailure(lifecycleError)}`);
|
|
5657
|
+
}
|
|
5658
|
+
if (finalized && ownedRecord) {
|
|
5659
|
+
try {
|
|
5660
|
+
const bytes = readFileSync(activeRecordPath, "utf-8");
|
|
5661
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
5662
|
+
if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
|
|
5663
|
+
rmSync(activeRecordPath, { force: true });
|
|
5664
|
+
}
|
|
5665
|
+
catch { }
|
|
5666
|
+
}
|
|
5667
|
+
if (payload.readyPath)
|
|
5668
|
+
writeDetachedLeaderReport(payload.readyPath, {
|
|
5669
|
+
version: 1, kind: "failed", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
|
|
5670
|
+
leaderPid: process.pid, finalized, error: failure instanceof Error ? failure.message : String(failure),
|
|
5671
|
+
});
|
|
5672
|
+
throw failure;
|
|
5673
|
+
}
|
|
5674
|
+
finally {
|
|
5675
|
+
await closeLaunchSessionBindingOnce(binding);
|
|
5676
|
+
}
|
|
4560
5677
|
}
|
|
4561
5678
|
/**
|
|
4562
5679
|
* postLaunch: Clean up after Codex exits.
|
|
4563
5680
|
* Each step is independently fault-tolerant (try/catch per step).
|
|
4564
5681
|
*/
|
|
4565
|
-
export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup) {
|
|
4566
|
-
|
|
4567
|
-
|
|
5682
|
+
export async function postLaunch(cwd, sessionId, binding, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup, onFinalized) {
|
|
5683
|
+
const sessionStartedAt = binding.startedAt;
|
|
5684
|
+
// Pointer authority is consumed before any terminal cleanup. A denied or
|
|
5685
|
+
// unproven binding must leave every cleanup target untouched.
|
|
5686
|
+
let terminalError;
|
|
4568
5687
|
try {
|
|
4569
|
-
const
|
|
4570
|
-
|
|
5688
|
+
const finalization = await finalizeBoundOnce(binding, "post-launch", cwd);
|
|
5689
|
+
if (!finalization.finalized)
|
|
5690
|
+
terminalError = new Error(`bound session finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
|
|
5691
|
+
const closeFailure = finalization.cleanup.capability.find((evidence) => evidence.status === "failed");
|
|
5692
|
+
if (closeFailure)
|
|
5693
|
+
terminalError = new Error(`bound session retained close failed: ${closeFailure.error?.message ?? "unknown error"}`);
|
|
4571
5694
|
}
|
|
4572
|
-
catch (
|
|
4573
|
-
|
|
4574
|
-
// Non-fatal
|
|
5695
|
+
catch (error) {
|
|
5696
|
+
terminalError = error;
|
|
4575
5697
|
}
|
|
5698
|
+
if (terminalError) {
|
|
5699
|
+
// Denial diagnostics are complete; do not mutate ordinary lifecycle state.
|
|
5700
|
+
await closeLaunchSessionBindingOnce(binding);
|
|
5701
|
+
throw terminalError;
|
|
5702
|
+
}
|
|
5703
|
+
await onFinalized?.();
|
|
4576
5704
|
// 0. Reap MCP orphans left behind by the session that just exited.
|
|
4577
5705
|
await reapPostLaunchOrphanedMcpProcesses();
|
|
4578
5706
|
// 0. Flush fallback watcher once to reduce race with fast codex exit.
|
|
@@ -4623,13 +5751,6 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
|
|
|
4623
5751
|
catch (err) {
|
|
4624
5752
|
console.error(`[omx] postLaunch: model instructions cleanup failed: ${err instanceof Error ? err.message : err}`);
|
|
4625
5753
|
}
|
|
4626
|
-
// 2. Archive session (write history, delete session.json)
|
|
4627
|
-
try {
|
|
4628
|
-
await writeSessionEnd(cwd, sessionId);
|
|
4629
|
-
}
|
|
4630
|
-
catch (err) {
|
|
4631
|
-
console.error(`[omx] postLaunch: session archive failed: ${err instanceof Error ? err.message : err}`);
|
|
4632
|
-
}
|
|
4633
5754
|
// 2.5. Best-effort wiki session capture
|
|
4634
5755
|
try {
|
|
4635
5756
|
const { onSessionEnd } = await import("../wiki/lifecycle.js");
|
|
@@ -4701,10 +5822,59 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
|
|
|
4701
5822
|
logCliOperationFailure(err);
|
|
4702
5823
|
// Non-fatal
|
|
4703
5824
|
}
|
|
5825
|
+
const closeEvidence = await closeLaunchSessionBindingOnce(binding);
|
|
5826
|
+
if (closeEvidence.status === "failed") {
|
|
5827
|
+
throw new Error(`bound session retained close failed: ${closeEvidence.error?.message ?? "unknown error"}`);
|
|
5828
|
+
}
|
|
4704
5829
|
}
|
|
4705
|
-
export async function runDetachedSessionPostLaunch(cwd, sessionId,
|
|
4706
|
-
|
|
4707
|
-
|
|
5830
|
+
export async function runDetachedSessionPostLaunch(cwd, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, releaseMarkerPath) {
|
|
5831
|
+
// The detached child is intentionally unbound: it must never perform parent
|
|
5832
|
+
// pointer finalization or cleanup after the release barrier. It still owns
|
|
5833
|
+
// every non-pointer terminal lifecycle responsibility.
|
|
5834
|
+
const failures = [];
|
|
5835
|
+
const strict = async (operation) => {
|
|
5836
|
+
try {
|
|
5837
|
+
await operation();
|
|
5838
|
+
}
|
|
5839
|
+
catch (error) {
|
|
5840
|
+
failures.push(error);
|
|
5841
|
+
}
|
|
5842
|
+
};
|
|
5843
|
+
await strict(() => reapPostLaunchOrphanedMcpProcesses());
|
|
5844
|
+
await strict(() => flushNotifyFallbackOnce(cwd, { sessionId }));
|
|
5845
|
+
await strict(() => stopNotifyFallbackWatcher(cwd));
|
|
5846
|
+
await strict(() => flushHookDerivedWatcherOnce(cwd));
|
|
5847
|
+
await strict(() => stopHookDerivedWatcher(cwd));
|
|
5848
|
+
if (projectLocalCodexHomeForCleanup) {
|
|
5849
|
+
await strict(() => cleanCodexModelAvailabilityNuxIfNeeded(join(projectLocalCodexHomeForCleanup, "config.toml")));
|
|
5850
|
+
}
|
|
5851
|
+
await strict(() => removeSessionModelInstructionsFile(cwd, sessionId));
|
|
5852
|
+
await strict(async () => {
|
|
5853
|
+
const { onSessionEnd } = await import("../wiki/lifecycle.js");
|
|
5854
|
+
onSessionEnd({ cwd, session_id: sessionId });
|
|
5855
|
+
});
|
|
5856
|
+
await strict(() => cleanupPostLaunchModeStateFiles(cwd, sessionId));
|
|
5857
|
+
await strict(async () => {
|
|
5858
|
+
const { notifyLifecycle } = await import("../notifications/index.js");
|
|
5859
|
+
await notifyLifecycle("session-end", {
|
|
5860
|
+
sessionId, projectPath: cwd, projectName: basename(cwd), reason: "session_exit",
|
|
5861
|
+
});
|
|
5862
|
+
});
|
|
5863
|
+
await strict(async () => {
|
|
5864
|
+
const { markOwnedTeamsLeaderSessionStopped } = await import("../team/state.js");
|
|
5865
|
+
await markOwnedTeamsLeaderSessionStopped(cwd, sessionId);
|
|
5866
|
+
});
|
|
5867
|
+
await strict(() => emitNativeHookEvent(cwd, "session-end", {
|
|
5868
|
+
session_id: sessionId,
|
|
5869
|
+
context: buildNativeHookBaseContext(cwd, sessionId, "finished", {
|
|
5870
|
+
project_path: cwd, project_name: basename(cwd), reason: "session_exit", status: "finished",
|
|
5871
|
+
}),
|
|
5872
|
+
}));
|
|
5873
|
+
await strict(() => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
|
|
5874
|
+
if (releaseMarkerPath)
|
|
5875
|
+
await strict(() => rm(releaseMarkerPath, { force: true }));
|
|
5876
|
+
if (failures.length > 0)
|
|
5877
|
+
throw new AggregateError(failures, "detached post-launch lifecycle cleanup failed");
|
|
4708
5878
|
}
|
|
4709
5879
|
async function emitNativeHookEvent(cwd, event, opts = {}) {
|
|
4710
5880
|
const payload = buildHookEvent(event, {
|
|
@@ -5143,14 +6313,13 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5143
6313
|
if (!sourceCwd || !runDir)
|
|
5144
6314
|
return;
|
|
5145
6315
|
try {
|
|
5146
|
-
if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
|
|
5147
|
-
(!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
|
|
6316
|
+
if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
|
|
6317
|
+
&& (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
|
|
5148
6318
|
return;
|
|
5149
6319
|
const resolvedRunDir = resolve(runDir);
|
|
5150
6320
|
if (resolvedRunDir !== canonicalRunsRoot
|
|
5151
|
-
&& !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
|
|
6321
|
+
&& !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
|
|
5152
6322
|
return;
|
|
5153
|
-
}
|
|
5154
6323
|
runDirs.add(resolvedRunDir);
|
|
5155
6324
|
}
|
|
5156
6325
|
catch {
|
|
@@ -5203,7 +6372,7 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5203
6372
|
for (const dir of candidateDirs) {
|
|
5204
6373
|
const files = await readdir(dir).catch(() => []);
|
|
5205
6374
|
for (const file of files) {
|
|
5206
|
-
if (!
|
|
6375
|
+
if (!isModeStateFilename(file))
|
|
5207
6376
|
continue;
|
|
5208
6377
|
const path = join(dir, file);
|
|
5209
6378
|
if (seenPaths.has(path))
|
|
@@ -5219,41 +6388,362 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5219
6388
|
}
|
|
5220
6389
|
return refs.sort((a, b) => a.mode.localeCompare(b.mode));
|
|
5221
6390
|
}
|
|
5222
|
-
|
|
5223
|
-
const
|
|
5224
|
-
|
|
6391
|
+
function isCanonicalPathWithin(parent, child, allowEqual = false) {
|
|
6392
|
+
const rel = relative(parent, child);
|
|
6393
|
+
if (rel === "")
|
|
6394
|
+
return allowEqual;
|
|
6395
|
+
return rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel);
|
|
6396
|
+
}
|
|
6397
|
+
async function selectAuthorizedHookVisibleRunDirState(cwd) {
|
|
6398
|
+
const runsRoot = resolveMadmaxRunsRoot(process.env);
|
|
6399
|
+
const activeDir = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR);
|
|
6400
|
+
const canonicalCwd = canonicalizePathForRunDirMatch(cwd);
|
|
6401
|
+
let canonicalRunsRoot;
|
|
6402
|
+
try {
|
|
6403
|
+
canonicalRunsRoot = realpathSync(resolve(runsRoot));
|
|
6404
|
+
}
|
|
6405
|
+
catch {
|
|
6406
|
+
return null;
|
|
6407
|
+
}
|
|
6408
|
+
const candidates = [];
|
|
6409
|
+
const files = await readdir(activeDir).catch(() => []);
|
|
6410
|
+
for (const file of files) {
|
|
6411
|
+
if (!file.endsWith(".json"))
|
|
6412
|
+
continue;
|
|
6413
|
+
const record = readMadmaxDetachedActiveRecord(join(activeDir, file));
|
|
6414
|
+
if (!record || file !== `${record.context_key}.json`)
|
|
6415
|
+
continue;
|
|
6416
|
+
if (!record.session_id || normalizeSessionId(record.session_id) !== record.session_id)
|
|
6417
|
+
continue;
|
|
6418
|
+
if (!hasMatchingMadmaxDetachedRuntimeBinding(record))
|
|
6419
|
+
continue;
|
|
6420
|
+
if (canonicalizePathForRunDirMatch(record.source_cwd) !== canonicalCwd
|
|
6421
|
+
&& (!record.worktree_cwd || canonicalizePathForRunDirMatch(record.worktree_cwd) !== canonicalCwd))
|
|
6422
|
+
continue;
|
|
6423
|
+
try {
|
|
6424
|
+
const canonicalRunDir = realpathSync(resolve(record.run_dir));
|
|
6425
|
+
if (!isCanonicalPathWithin(canonicalRunsRoot, canonicalRunDir)) {
|
|
6426
|
+
throw new Error("run directory escapes the authorized runs root");
|
|
6427
|
+
}
|
|
6428
|
+
const stateDir = realpathSync(join(canonicalRunDir, ".omx", "state"));
|
|
6429
|
+
if (!isCanonicalPathWithin(canonicalRunDir, stateDir)) {
|
|
6430
|
+
throw new Error("state directory escapes the authorized run directory");
|
|
6431
|
+
}
|
|
6432
|
+
const session = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
|
|
6433
|
+
if (session.session_id !== record.session_id)
|
|
6434
|
+
throw new Error("run session pointer changed");
|
|
6435
|
+
const sessionDir = realpathSync(join(stateDir, "sessions", record.session_id));
|
|
6436
|
+
if (!isCanonicalPathWithin(stateDir, sessionDir)) {
|
|
6437
|
+
throw new Error("session directory escapes the authorized state directory");
|
|
6438
|
+
}
|
|
6439
|
+
candidates.push({ sessionDir, sessionId: record.session_id, record });
|
|
6440
|
+
}
|
|
6441
|
+
catch (err) {
|
|
6442
|
+
throw new Error(`Refusing cancellation because detached run authority is invalid: ${record.run_dir}.`, { cause: err });
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
if (candidates.length > 1)
|
|
6446
|
+
throw new Error("Refusing cancellation because multiple detached run authorities match.");
|
|
6447
|
+
if (candidates.length === 0)
|
|
6448
|
+
return null;
|
|
6449
|
+
const [{ sessionDir, sessionId, record }] = candidates;
|
|
6450
|
+
const refs = [];
|
|
6451
|
+
const stateFiles = await readdir(sessionDir).catch(() => []);
|
|
6452
|
+
for (const file of stateFiles) {
|
|
6453
|
+
if (!isModeStateFilename(file))
|
|
6454
|
+
continue;
|
|
6455
|
+
const path = join(sessionDir, file);
|
|
6456
|
+
try {
|
|
6457
|
+
const fileStat = lstatSync(path);
|
|
6458
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
6459
|
+
throw new Error(`Refusing cancellation through non-regular run state target: ${path}.`);
|
|
6460
|
+
}
|
|
6461
|
+
const canonicalFile = realpathSync(path);
|
|
6462
|
+
if (!isCanonicalPathWithin(sessionDir, canonicalFile)) {
|
|
6463
|
+
throw new Error(`Refusing cancellation outside authorized run session: ${path}.`);
|
|
6464
|
+
}
|
|
6465
|
+
refs.push({
|
|
6466
|
+
mode: file.slice(0, -"-state.json".length),
|
|
6467
|
+
path: canonicalFile,
|
|
6468
|
+
scope: "session",
|
|
6469
|
+
});
|
|
6470
|
+
}
|
|
6471
|
+
catch (err) {
|
|
6472
|
+
throw new Error(`Refusing cancellation because detached run state authority is invalid: ${path}.`, { cause: err });
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
return { refs: refs.sort((a, b) => a.mode.localeCompare(b.mode)), sessionId, sessionDir, record };
|
|
6476
|
+
}
|
|
6477
|
+
function assertCancellationAuthorityPath(baseStateDir, authorityRoot) {
|
|
6478
|
+
const resolvedBase = resolve(baseStateDir);
|
|
6479
|
+
const resolvedAuthority = resolve(authorityRoot);
|
|
6480
|
+
const baseStat = lstatSync(resolvedBase);
|
|
6481
|
+
if (!baseStat.isDirectory() || baseStat.isSymbolicLink() || realpathSync(resolvedBase) !== resolvedBase) {
|
|
6482
|
+
throw new Error(`Refusing cancellation through symlinked state authority root: ${baseStateDir}.`);
|
|
6483
|
+
}
|
|
6484
|
+
if (!isCanonicalPathWithin(resolvedBase, resolvedAuthority, true)) {
|
|
6485
|
+
throw new Error(`Refusing cancellation outside base state authority: ${authorityRoot}.`);
|
|
6486
|
+
}
|
|
6487
|
+
let current = resolvedBase;
|
|
6488
|
+
const rel = relative(resolvedBase, resolvedAuthority);
|
|
6489
|
+
for (const component of rel ? rel.split(/[\\/]+/) : []) {
|
|
6490
|
+
current = join(current, component);
|
|
6491
|
+
const componentStat = lstatSync(current);
|
|
6492
|
+
if (!componentStat.isDirectory() || componentStat.isSymbolicLink() || realpathSync(current) !== current) {
|
|
6493
|
+
throw new Error(`Refusing cancellation through symlinked authority component: ${current}.`);
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
return resolvedAuthority;
|
|
6497
|
+
}
|
|
6498
|
+
function collectCancellationOwnerIds(baseStateDir, sessionId) {
|
|
6499
|
+
const ownerIds = new Set();
|
|
6500
|
+
if (sessionId)
|
|
6501
|
+
ownerIds.add(sessionId);
|
|
6502
|
+
try {
|
|
6503
|
+
const pointer = JSON.parse(readFileSync(join(baseStateDir, "session.json"), "utf-8"));
|
|
6504
|
+
for (const field of [
|
|
6505
|
+
"session_id",
|
|
6506
|
+
"native_session_id",
|
|
6507
|
+
"codex_session_id",
|
|
6508
|
+
"owner_omx_session_id",
|
|
6509
|
+
"owner_codex_session_id",
|
|
6510
|
+
]) {
|
|
6511
|
+
const normalized = normalizeSessionId(pointer[field]);
|
|
6512
|
+
if (normalized)
|
|
6513
|
+
ownerIds.add(normalized);
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
catch { }
|
|
6517
|
+
return ownerIds;
|
|
6518
|
+
}
|
|
6519
|
+
function assertCancellationOwnerMetadata(value, ownerIds, path) {
|
|
6520
|
+
if (Object.prototype.hasOwnProperty.call(value, "session_id")) {
|
|
6521
|
+
const sessionId = normalizeSessionId(value.session_id);
|
|
6522
|
+
if (!sessionId || !ownerIds.has(sessionId)) {
|
|
6523
|
+
throw new Error(`Refusing contradictory session_id in ${path}.`);
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6526
|
+
for (const ownerField of ["owner_omx_session_id", "owner_codex_session_id"]) {
|
|
6527
|
+
if (!Object.prototype.hasOwnProperty.call(value, ownerField))
|
|
6528
|
+
continue;
|
|
6529
|
+
const rawOwner = value[ownerField];
|
|
6530
|
+
const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
|
|
6531
|
+
const owner = normalizeSessionId(rawOwner);
|
|
6532
|
+
if (!blankOwner && (!owner || !ownerIds.has(owner))) {
|
|
6533
|
+
throw new Error(`Refusing contradictory ${ownerField} in ${path}.`);
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
async function resolveExactSessionCancellationAuthority(cwd, baseStateDir, sessionId) {
|
|
6538
|
+
if (!sessionId)
|
|
6539
|
+
return undefined;
|
|
6540
|
+
let pointer;
|
|
6541
|
+
let authorityCwd = cwd;
|
|
6542
|
+
try {
|
|
6543
|
+
const rawPointer = JSON.parse(await readFile(join(baseStateDir, "session.json"), "utf-8"));
|
|
6544
|
+
if (typeof rawPointer.cwd === "string" && rawPointer.cwd.trim())
|
|
6545
|
+
authorityCwd = rawPointer.cwd.trim();
|
|
6546
|
+
pointer = await readSessionPointer(resolveSessionPointerContext(authorityCwd));
|
|
6547
|
+
}
|
|
6548
|
+
catch {
|
|
6549
|
+
return undefined;
|
|
6550
|
+
}
|
|
6551
|
+
const nativeSessionId = normalizeSessionId(pointer.state?.native_session_id);
|
|
6552
|
+
if (pointer.status !== "usable" || pointer.state?.session_id !== sessionId || !nativeSessionId)
|
|
6553
|
+
return undefined;
|
|
6554
|
+
try {
|
|
6555
|
+
const ownerEvidence = await readNativeSessionOwnerEvidence(authorityCwd, nativeSessionId);
|
|
6556
|
+
if (ownerEvidence.status !== "usable")
|
|
6557
|
+
return undefined;
|
|
6558
|
+
}
|
|
6559
|
+
catch {
|
|
6560
|
+
return undefined;
|
|
6561
|
+
}
|
|
6562
|
+
return { sessionId, nativeSessionId, authorityCwd };
|
|
6563
|
+
}
|
|
6564
|
+
function assertExactSkillOwner(value, authority, path, allowStaleTopCodexOwner = false) {
|
|
6565
|
+
if (Object.prototype.hasOwnProperty.call(value, "session_id")
|
|
6566
|
+
&& normalizeSessionId(value.session_id) !== authority.sessionId) {
|
|
6567
|
+
throw new Error(`Refusing contradictory session_id in ${path}.`);
|
|
6568
|
+
}
|
|
6569
|
+
if (Object.prototype.hasOwnProperty.call(value, "owner_omx_session_id")) {
|
|
6570
|
+
const rawOwner = value.owner_omx_session_id;
|
|
6571
|
+
const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
|
|
6572
|
+
const owner = normalizeSessionId(rawOwner);
|
|
6573
|
+
if (!blankOwner && (!owner || owner !== authority.sessionId)) {
|
|
6574
|
+
throw new Error(`Refusing contradictory owner_omx_session_id in ${path}.`);
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6577
|
+
if (Object.prototype.hasOwnProperty.call(value, "owner_codex_session_id")) {
|
|
6578
|
+
const rawOwner = value.owner_codex_session_id;
|
|
6579
|
+
const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
|
|
6580
|
+
const owner = normalizeSessionId(rawOwner);
|
|
6581
|
+
if (!blankOwner && (!owner || (!allowStaleTopCodexOwner && owner !== authority.nativeSessionId))) {
|
|
6582
|
+
throw new Error(`Refusing contradictory owner_codex_session_id in ${path}.`);
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
function assertCompatibleNestedSkillOwners(value, authority, path) {
|
|
6587
|
+
if (!Object.prototype.hasOwnProperty.call(value, "active_skills"))
|
|
6588
|
+
return;
|
|
6589
|
+
if (!Array.isArray(value.active_skills)) {
|
|
6590
|
+
throw new Error(`Refusing malformed active_skills in ${path}.`);
|
|
6591
|
+
}
|
|
6592
|
+
for (const [index, skill] of value.active_skills.entries()) {
|
|
6593
|
+
const entryPath = `${path} active_skills[${index}]`;
|
|
6594
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
|
|
6595
|
+
throw new Error(`Refusing malformed active_skills entry ${index} in ${path}.`);
|
|
6596
|
+
}
|
|
6597
|
+
const entry = skill;
|
|
6598
|
+
if (typeof entry.skill !== "string" || entry.skill.trim() === "") {
|
|
6599
|
+
throw new Error(`Refusing malformed skill in ${entryPath}.`);
|
|
6600
|
+
}
|
|
6601
|
+
for (const field of ["active"]) {
|
|
6602
|
+
if (Object.prototype.hasOwnProperty.call(entry, field) && typeof entry[field] !== "boolean") {
|
|
6603
|
+
throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
if (Object.prototype.hasOwnProperty.call(entry, "phase") && typeof entry.phase !== "string") {
|
|
6607
|
+
throw new Error(`Refusing malformed phase in ${entryPath}.`);
|
|
6608
|
+
}
|
|
6609
|
+
for (const [field, fieldValue] of Object.entries(entry)) {
|
|
6610
|
+
if (field.endsWith("_at") && typeof fieldValue !== "string") {
|
|
6611
|
+
throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
assertExactSkillOwner(entry, authority, entryPath);
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
async function cancelModes(args = [], options = {}) {
|
|
6618
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5225
6619
|
const nowIso = new Date().toISOString();
|
|
5226
|
-
|
|
6620
|
+
if (args.length > 1 || args.some((arg) => arg !== "--force")) {
|
|
6621
|
+
const unsupported = args.find((arg) => arg !== "--force") ?? args[1] ?? "";
|
|
6622
|
+
throw new Error(unsupported === "--all"
|
|
6623
|
+
? "omx cancel --all is not supported; broad workspace cancellation requires a separate command."
|
|
6624
|
+
: `Unknown cancel argument: ${unsupported}`);
|
|
6625
|
+
}
|
|
6626
|
+
const force = args.length === 1;
|
|
5227
6627
|
try {
|
|
5228
6628
|
const writableScope = await resolveWritableStateScope(cwd);
|
|
5229
|
-
const
|
|
6629
|
+
const baseStateDir = getBaseStateDir(cwd);
|
|
6630
|
+
const exactAuthority = writableScope.source === "session"
|
|
6631
|
+
? await resolveExactSessionCancellationAuthority(cwd, baseStateDir, writableScope.sessionId)
|
|
6632
|
+
: undefined;
|
|
6633
|
+
const expectedOwnerIds = collectCancellationOwnerIds(baseStateDir, writableScope.sessionId);
|
|
6634
|
+
const loadStates = async (refs, authorityRoot, ownerIds = expectedOwnerIds) => {
|
|
5230
6635
|
const loaded = new Map();
|
|
6636
|
+
if (refs.length === 0)
|
|
6637
|
+
return loaded;
|
|
6638
|
+
const canonicalAuthorityRoot = assertCancellationAuthorityPath(authorityRoot === writableScope.stateDir ? baseStateDir : authorityRoot, authorityRoot);
|
|
5231
6639
|
for (const ref of refs) {
|
|
5232
|
-
const
|
|
6640
|
+
const fileStat = lstatSync(ref.path);
|
|
6641
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
6642
|
+
throw new Error(`Refusing cancellation through non-regular state target ${ref.path}.`);
|
|
6643
|
+
}
|
|
6644
|
+
const canonicalPath = realpathSync(ref.path);
|
|
6645
|
+
const canonicalParent = realpathSync(dirname(ref.path));
|
|
6646
|
+
if (!isCanonicalPathWithin(canonicalAuthorityRoot, canonicalParent, true)
|
|
6647
|
+
|| !isCanonicalPathWithin(canonicalAuthorityRoot, canonicalPath)) {
|
|
6648
|
+
throw new Error(`Refusing cancellation outside authorized state root: ${ref.path}.`);
|
|
6649
|
+
}
|
|
6650
|
+
const content = await readFile(canonicalPath, "utf-8");
|
|
5233
6651
|
let parsedState;
|
|
5234
6652
|
try {
|
|
5235
|
-
|
|
6653
|
+
const parsed = JSON.parse(content);
|
|
6654
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6655
|
+
throw new Error("state must be a JSON object");
|
|
6656
|
+
}
|
|
6657
|
+
parsedState = parsed;
|
|
5236
6658
|
}
|
|
5237
6659
|
catch (err) {
|
|
5238
6660
|
logCliOperationFailure(err);
|
|
5239
|
-
|
|
6661
|
+
throw new Error(`Refusing partial cancellation because ${ref.path} is malformed.`, { cause: err });
|
|
6662
|
+
}
|
|
6663
|
+
if (typeof parsedState.mode === "string" && parsedState.mode !== ref.mode) {
|
|
6664
|
+
throw new Error(`Refusing contradictory mode state in ${ref.path}.`);
|
|
6665
|
+
}
|
|
6666
|
+
if (ref.mode === SKILL_ACTIVE_STATE_MODE && exactAuthority) {
|
|
6667
|
+
const hasTopCodexOwner = Object.prototype.hasOwnProperty.call(parsedState, "owner_codex_session_id");
|
|
6668
|
+
const displacedOwnerId = hasTopCodexOwner
|
|
6669
|
+
? normalizeSessionId(parsedState.owner_codex_session_id)
|
|
6670
|
+
: undefined;
|
|
6671
|
+
const blankTopCodexOwner = typeof parsedState.owner_codex_session_id === "string"
|
|
6672
|
+
&& parsedState.owner_codex_session_id.trim() === "";
|
|
6673
|
+
if (hasTopCodexOwner && !blankTopCodexOwner && !displacedOwnerId) {
|
|
6674
|
+
throw new Error(`Refusing contradictory owner_codex_session_id in ${ref.path}.`);
|
|
6675
|
+
}
|
|
6676
|
+
assertExactSkillOwner(parsedState, exactAuthority, ref.path, hasTopCodexOwner);
|
|
6677
|
+
assertCompatibleNestedSkillOwners(parsedState, exactAuthority, ref.path);
|
|
6678
|
+
if (displacedOwnerId && displacedOwnerId !== exactAuthority.nativeSessionId) {
|
|
6679
|
+
let displacedEvidence;
|
|
6680
|
+
try {
|
|
6681
|
+
displacedEvidence = await readNativeSessionOwnerEvidence(exactAuthority.authorityCwd, displacedOwnerId);
|
|
6682
|
+
}
|
|
6683
|
+
catch (error) {
|
|
6684
|
+
throw new Error(`Refusing ambiguous displaced owner evidence for ${ref.path}.`, { cause: error });
|
|
6685
|
+
}
|
|
6686
|
+
if (displacedEvidence.status !== "absent" && displacedEvidence.status !== "stale-dead") {
|
|
6687
|
+
throw new Error(`Refusing non-stale displaced owner evidence for ${ref.path}.`);
|
|
6688
|
+
}
|
|
6689
|
+
parsedState.owner_codex_session_id = exactAuthority.nativeSessionId;
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
else {
|
|
6693
|
+
assertCancellationOwnerMetadata(parsedState, ownerIds, ref.path);
|
|
6694
|
+
if (Object.prototype.hasOwnProperty.call(parsedState, "active_skills")) {
|
|
6695
|
+
if (!Array.isArray(parsedState.active_skills)) {
|
|
6696
|
+
throw new Error(`Refusing malformed active_skills in ${ref.path}.`);
|
|
6697
|
+
}
|
|
6698
|
+
for (const [index, skill] of parsedState.active_skills.entries()) {
|
|
6699
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
|
|
6700
|
+
throw new Error(`Refusing malformed active_skills entry ${index} in ${ref.path}.`);
|
|
6701
|
+
}
|
|
6702
|
+
assertCancellationOwnerMetadata(skill, ownerIds, `${ref.path} active_skills[${index}]`);
|
|
6703
|
+
}
|
|
6704
|
+
}
|
|
5240
6705
|
}
|
|
5241
6706
|
loaded.set(ref.mode, {
|
|
5242
|
-
path:
|
|
6707
|
+
path: canonicalPath,
|
|
5243
6708
|
scope: ref.scope,
|
|
5244
6709
|
state: parsedState,
|
|
6710
|
+
originalContent: content,
|
|
6711
|
+
dev: fileStat.dev,
|
|
6712
|
+
ino: fileStat.ino,
|
|
5245
6713
|
});
|
|
5246
6714
|
}
|
|
5247
6715
|
return loaded;
|
|
5248
6716
|
};
|
|
5249
|
-
|
|
6717
|
+
const preferredRefs = writableScope.source === "root"
|
|
6718
|
+
? (await readdir(writableScope.stateDir).catch(() => []))
|
|
6719
|
+
.filter((file) => isModeStateFilename(file))
|
|
6720
|
+
.map((file) => ({
|
|
6721
|
+
mode: file.slice(0, -"-state.json".length),
|
|
6722
|
+
path: join(writableScope.stateDir, file),
|
|
6723
|
+
scope: "root",
|
|
6724
|
+
}))
|
|
6725
|
+
: await listModeStateFilesWithScopePreference(cwd, writableScope.sessionId);
|
|
6726
|
+
const hasPreferredSessionStateFiles = preferredRefs.some((ref) => ref.scope === "session");
|
|
6727
|
+
const targetRefs = writableScope.source === "session"
|
|
6728
|
+
? preferredRefs.filter((ref) => ref.scope === "session")
|
|
6729
|
+
: preferredRefs;
|
|
6730
|
+
let runSelection = null;
|
|
6731
|
+
let states = await loadStates(targetRefs, writableScope.stateDir, expectedOwnerIds);
|
|
5250
6732
|
const hasActiveWorkflowMode = (entries) => [...entries.entries()].some(([mode, entry]) => mode !== SKILL_ACTIVE_STATE_MODE && entry.state.active === true);
|
|
5251
|
-
if (
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
6733
|
+
if (writableScope.source === "root"
|
|
6734
|
+
&& !hasActiveWorkflowMode(states)
|
|
6735
|
+
&& !hasPreferredSessionStateFiles) {
|
|
6736
|
+
runSelection = await selectAuthorizedHookVisibleRunDirState(cwd);
|
|
6737
|
+
if (runSelection) {
|
|
6738
|
+
const runOwnerIds = new Set([runSelection.sessionId]);
|
|
6739
|
+
const runDirStates = await loadStates(runSelection.refs, runSelection.sessionDir, runOwnerIds);
|
|
6740
|
+
if (hasActiveWorkflowMode(runDirStates))
|
|
6741
|
+
states = runDirStates;
|
|
6742
|
+
else
|
|
6743
|
+
runSelection = null;
|
|
6744
|
+
}
|
|
5255
6745
|
}
|
|
5256
|
-
const currentSessionId = writableScope.sessionId ?? "";
|
|
6746
|
+
const currentSessionId = writableScope.sessionId ?? runSelection?.sessionId ?? "";
|
|
5257
6747
|
const changed = new Set();
|
|
5258
6748
|
const reported = new Set();
|
|
5259
6749
|
const cancelMode = (mode, phase = "cancelled", reportIfWasActive = true) => {
|
|
@@ -5284,13 +6774,74 @@ async function cancelModes(args = []) {
|
|
|
5284
6774
|
if (reportIfWasActive && wasActive && mode !== SKILL_ACTIVE_STATE_MODE)
|
|
5285
6775
|
reported.add(mode);
|
|
5286
6776
|
};
|
|
5287
|
-
const
|
|
6777
|
+
const linkedRalphMode = (state) => {
|
|
6778
|
+
if (state.linked_ultrawork === true || state.linked_mode === "ultrawork")
|
|
6779
|
+
return "ultrawork";
|
|
6780
|
+
if (state.linked_ecomode === true || state.linked_mode === "ecomode")
|
|
6781
|
+
return "ecomode";
|
|
6782
|
+
return undefined;
|
|
6783
|
+
};
|
|
6784
|
+
const cancelRalphSkillEntries = (linkedMode) => {
|
|
6785
|
+
const entry = states.get(SKILL_ACTIVE_STATE_MODE);
|
|
6786
|
+
if (!entry)
|
|
6787
|
+
return;
|
|
6788
|
+
const matchingSkills = new Set(["ralph", ...(linkedMode ? [linkedMode] : [])]);
|
|
6789
|
+
const activeSkills = Array.isArray(entry.state.active_skills)
|
|
6790
|
+
? entry.state.active_skills
|
|
6791
|
+
: [];
|
|
6792
|
+
let changedSkillEntries = false;
|
|
6793
|
+
const nextSkills = activeSkills.map((skill) => {
|
|
6794
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill))
|
|
6795
|
+
return skill;
|
|
6796
|
+
const skillEntry = skill;
|
|
6797
|
+
if (skillEntry.active === false || typeof skillEntry.skill !== "string" || !matchingSkills.has(skillEntry.skill)) {
|
|
6798
|
+
return skill;
|
|
6799
|
+
}
|
|
6800
|
+
changedSkillEntries = true;
|
|
6801
|
+
return { ...skillEntry, active: false, phase: "cancelled" };
|
|
6802
|
+
});
|
|
6803
|
+
const remainingActiveSkills = nextSkills.filter((skill) => (!!skill && typeof skill === "object" && !Array.isArray(skill)
|
|
6804
|
+
&& skill.active !== false));
|
|
6805
|
+
if (!changedSkillEntries) {
|
|
6806
|
+
if (entry.state.active !== true)
|
|
6807
|
+
return;
|
|
6808
|
+
if (typeof entry.state.skill !== "string" || !matchingSkills.has(entry.state.skill))
|
|
6809
|
+
return;
|
|
6810
|
+
}
|
|
6811
|
+
entry.state.active_skills = nextSkills;
|
|
6812
|
+
entry.state.updated_at = nowIso;
|
|
6813
|
+
entry.state.last_turn_at = nowIso;
|
|
6814
|
+
if (remainingActiveSkills.length > 0) {
|
|
6815
|
+
const primary = remainingActiveSkills[0];
|
|
6816
|
+
entry.state.active = true;
|
|
6817
|
+
entry.state.skill = primary.skill;
|
|
6818
|
+
if (typeof primary.phase === "string") {
|
|
6819
|
+
entry.state.phase = primary.phase;
|
|
6820
|
+
entry.state.current_phase = primary.phase;
|
|
6821
|
+
}
|
|
6822
|
+
else {
|
|
6823
|
+
delete entry.state.phase;
|
|
6824
|
+
delete entry.state.current_phase;
|
|
6825
|
+
}
|
|
6826
|
+
delete entry.state.completed_at;
|
|
6827
|
+
}
|
|
6828
|
+
else {
|
|
6829
|
+
entry.state.active = false;
|
|
6830
|
+
entry.state.skill = "";
|
|
6831
|
+
entry.state.phase = "cancelled";
|
|
6832
|
+
entry.state.current_phase = "cancelled";
|
|
6833
|
+
entry.state.completed_at = nowIso;
|
|
6834
|
+
}
|
|
6835
|
+
changed.add(SKILL_ACTIVE_STATE_MODE);
|
|
6836
|
+
};
|
|
5288
6837
|
const ralph = states.get("ralph");
|
|
5289
6838
|
const hadActiveRalph = !!(ralph && ralph.state.active === true);
|
|
5290
6839
|
if (ralph && ralph.state.active === true) {
|
|
6840
|
+
const linkedMode = linkedRalphMode(ralph.state);
|
|
6841
|
+
if (linkedMode)
|
|
6842
|
+
cancelMode(linkedMode, "cancelled", true);
|
|
5291
6843
|
cancelMode("ralph", "cancelled", true);
|
|
5292
|
-
|
|
5293
|
-
cancelMode("ultrawork", "cancelled", true);
|
|
6844
|
+
cancelRalphSkillEntries(linkedMode);
|
|
5294
6845
|
}
|
|
5295
6846
|
if (!hadActiveRalph) {
|
|
5296
6847
|
for (const [mode, entry] of states.entries()) {
|
|
@@ -5298,11 +6849,11 @@ async function cancelModes(args = []) {
|
|
|
5298
6849
|
cancelMode(mode, "cancelled", true);
|
|
5299
6850
|
}
|
|
5300
6851
|
}
|
|
5301
|
-
|
|
5302
|
-
if (!
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
}
|
|
6852
|
+
const assertRunAuthority = () => {
|
|
6853
|
+
if (runSelection && !hasMatchingMadmaxDetachedRuntimeBinding(runSelection.record)) {
|
|
6854
|
+
throw new Error("Refusing cancellation because detached run authority changed.");
|
|
6855
|
+
}
|
|
6856
|
+
};
|
|
5306
6857
|
if (force && currentSessionId) {
|
|
5307
6858
|
const stopStateEntries = [...states.entries()].filter(([mode]) => mode === "native-stop");
|
|
5308
6859
|
for (const [, entry] of stopStateEntries) {
|
|
@@ -5313,10 +6864,85 @@ async function cancelModes(args = []) {
|
|
|
5313
6864
|
continue;
|
|
5314
6865
|
delete sessions[currentSessionId];
|
|
5315
6866
|
entry.state.sessions = sessions;
|
|
5316
|
-
await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
|
|
5317
6867
|
changed.add("native-stop");
|
|
5318
6868
|
}
|
|
5319
6869
|
}
|
|
6870
|
+
const orderedChanges = [...changed]
|
|
6871
|
+
.sort((left, right) => {
|
|
6872
|
+
if (left === "ralph")
|
|
6873
|
+
return 1;
|
|
6874
|
+
if (right === "ralph")
|
|
6875
|
+
return -1;
|
|
6876
|
+
return left.localeCompare(right);
|
|
6877
|
+
})
|
|
6878
|
+
.map((mode) => {
|
|
6879
|
+
const entry = states.get(mode);
|
|
6880
|
+
if (!entry)
|
|
6881
|
+
throw new Error(`Missing frozen cancellation entry for ${mode}.`);
|
|
6882
|
+
return { mode, entry, nextContent: JSON.stringify(entry.state, null, 2) };
|
|
6883
|
+
});
|
|
6884
|
+
const opened = [];
|
|
6885
|
+
try {
|
|
6886
|
+
for (const change of orderedChanges) {
|
|
6887
|
+
assertRunAuthority();
|
|
6888
|
+
const handle = await open(change.entry.path, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW);
|
|
6889
|
+
const currentStat = await handle.stat();
|
|
6890
|
+
const currentContent = await handle.readFile({ encoding: "utf-8" });
|
|
6891
|
+
if (!currentStat.isFile() || currentStat.dev !== change.entry.dev || currentStat.ino !== change.entry.ino) {
|
|
6892
|
+
await handle.close();
|
|
6893
|
+
throw new Error(`Refusing cancellation because state identity changed: ${change.entry.path}.`);
|
|
6894
|
+
}
|
|
6895
|
+
if (currentContent !== change.entry.originalContent) {
|
|
6896
|
+
await handle.close();
|
|
6897
|
+
throw new Error(`Refusing cancellation because state content changed: ${change.entry.path}.`);
|
|
6898
|
+
}
|
|
6899
|
+
opened.push({ ...change, handle });
|
|
6900
|
+
}
|
|
6901
|
+
const committed = [];
|
|
6902
|
+
const cancellationTestWriteFailureMode = options.testFaults?.writeFailureMode;
|
|
6903
|
+
const cancellationTestRollbackFailureMode = options.testFaults?.rollbackFailureMode;
|
|
6904
|
+
try {
|
|
6905
|
+
for (const openedEntry of opened) {
|
|
6906
|
+
assertRunAuthority();
|
|
6907
|
+
committed.push(openedEntry);
|
|
6908
|
+
if (cancellationTestWriteFailureMode === openedEntry.mode) {
|
|
6909
|
+
throw new Error(`Injected cancellation write failure for ${openedEntry.mode}.`);
|
|
6910
|
+
}
|
|
6911
|
+
await openedEntry.handle.truncate(0);
|
|
6912
|
+
await openedEntry.handle.write(openedEntry.nextContent, 0, "utf-8");
|
|
6913
|
+
await openedEntry.handle.sync();
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
catch (writeError) {
|
|
6917
|
+
let rollbackFailureCount = 0;
|
|
6918
|
+
const rollbackFailureSample = [];
|
|
6919
|
+
for (const committedEntry of committed.reverse()) {
|
|
6920
|
+
try {
|
|
6921
|
+
if (cancellationTestRollbackFailureMode === committedEntry.mode) {
|
|
6922
|
+
throw new Error(`Injected cancellation rollback failure for ${committedEntry.mode}.`);
|
|
6923
|
+
}
|
|
6924
|
+
await committedEntry.handle.truncate(0);
|
|
6925
|
+
await committedEntry.handle.write(committedEntry.entry.originalContent, 0, "utf-8");
|
|
6926
|
+
await committedEntry.handle.sync();
|
|
6927
|
+
}
|
|
6928
|
+
catch {
|
|
6929
|
+
rollbackFailureCount += 1;
|
|
6930
|
+
if (rollbackFailureSample.length < 3) {
|
|
6931
|
+
rollbackFailureSample.push(committedEntry.mode.replace(/[^A-Za-z0-9_-]/g, "_"));
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
if (rollbackFailureCount > 0) {
|
|
6936
|
+
const primaryMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
|
6937
|
+
const sample = rollbackFailureSample.length > 0 ? `:${rollbackFailureSample.join(",")}` : "";
|
|
6938
|
+
throw new Error(`${primaryMessage} (cancellation_rollback_failed:${rollbackFailureCount}${sample}).`, { cause: writeError });
|
|
6939
|
+
}
|
|
6940
|
+
throw writeError;
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6943
|
+
finally {
|
|
6944
|
+
await Promise.all(opened.map(({ handle }) => handle.close().catch(() => undefined)));
|
|
6945
|
+
}
|
|
5320
6946
|
for (const mode of reported) {
|
|
5321
6947
|
console.log(`Cancelled: ${mode}`);
|
|
5322
6948
|
}
|
|
@@ -5326,7 +6952,11 @@ async function cancelModes(args = []) {
|
|
|
5326
6952
|
}
|
|
5327
6953
|
catch (err) {
|
|
5328
6954
|
logCliOperationFailure(err);
|
|
5329
|
-
|
|
6955
|
+
process.exitCode = 1;
|
|
5330
6956
|
}
|
|
5331
6957
|
}
|
|
6958
|
+
/** Test-only entrypoint for deterministic transaction fault injection without runtime environment controls. */
|
|
6959
|
+
export async function cancelModesForTest(cwd, args, testFaults) {
|
|
6960
|
+
await cancelModes(args, { cwd, testFaults });
|
|
6961
|
+
}
|
|
5332
6962
|
//# sourceMappingURL=index.js.map
|