oh-my-codex 0.20.2 → 0.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +36 -18
- package/Cargo.toml +1 -1
- package/README.md +28 -2
- package/crates/omx-runtime/Cargo.toml +4 -0
- package/crates/omx-runtime/src/main.rs +415 -2
- package/crates/omx-runtime/tests/execution.rs +483 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +22 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +107 -22
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +63 -7
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +211 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +878 -463
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +8 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +107 -109
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +150 -8
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +95 -4
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +290 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +234 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2409 -746
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +20 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +67 -32
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +5 -7
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +83 -134
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +119 -47
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +71 -30
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +503 -30
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +27 -21
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +610 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3130 -365
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +204 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1500 -223
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +124 -37
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +17 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +102 -26
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +72 -155
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +85 -101
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +82 -338
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +360 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +2 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +67 -168
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +53 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +14448 -1480
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +78 -115
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +82 -33
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +32 -3
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +79 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +80 -4
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +3162 -26
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +61 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +462 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +8 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +73 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +58 -135
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +811 -632
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +197 -30
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3963 -428
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2405 -203
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +4099 -1039
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +4 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1527 -291
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1503 -387
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +85 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +672 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +163 -27
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1682 -376
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +114 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +25 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +67 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +16 -17
- package/skills/team/SKILL.md +13 -9
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
- package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +14238 -1429
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +91 -122
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
- package/src/scripts/notify-hook/team-worker.ts +82 -31
- package/src/scripts/notify-hook/tmux-injection.ts +33 -3
- package/src/scripts/notify-hook.ts +84 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +3608 -39
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { basename, dirname, join } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { HUD_TMUX_HEIGHT_LINES } from '../../hud/constants.js';
|
|
10
|
-
import { DETACHED_TMUX_HISTORY_LIMIT } from '../index.js';
|
|
9
|
+
import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_HEIGHT_LINES } from '../../hud/constants.js';
|
|
10
|
+
import { DETACHED_LAUNCH_CONTROL_PLANE_KEYS, DETACHED_TMUX_HISTORY_LIMIT, buildMadmaxDetachedLaunchContextKey } from '../index.js';
|
|
11
|
+
import { closeLaunchSessionBindingOnce, establishLaunchSessionBinding, finalizeBoundOnce, writeSessionStart, } from '../../hooks/session.js';
|
|
12
|
+
import { buildPtyScriptCommand, isRealScriptAvailable, isRealTmuxAvailable, withTempTmuxSession } from '../../team/__tests__/tmux-test-fixture.js';
|
|
11
13
|
const CLI_SPAWN_TIMEOUT_MS = 60_000;
|
|
14
|
+
const DARWIN_PTY_FINALIZATION_POLL_LIMIT = 600;
|
|
12
15
|
function buildRunOmxEnv(envOverrides) {
|
|
13
16
|
const env = { ...process.env };
|
|
14
17
|
for (const key of Object.keys(env)) {
|
|
@@ -50,8 +53,10 @@ function normalizeDarwinTmpPath(value) {
|
|
|
50
53
|
function escapeRegExp(value) {
|
|
51
54
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
55
|
}
|
|
53
|
-
function shouldSkipForSpawnPermissions(
|
|
54
|
-
|
|
56
|
+
function shouldSkipForSpawnPermissions(_err) {
|
|
57
|
+
// These integration fixtures are contractual: EPERM/EACCES is a failure,
|
|
58
|
+
// not a pass. Node's test runner has no portable dynamic skip here.
|
|
59
|
+
return false;
|
|
55
60
|
}
|
|
56
61
|
async function createGitRepo(wd) {
|
|
57
62
|
const repo = join(wd, 'repo');
|
|
@@ -68,17 +73,167 @@ async function writeExecutable(path, content) {
|
|
|
68
73
|
await writeFile(path, content);
|
|
69
74
|
await chmod(path, 0o755);
|
|
70
75
|
}
|
|
76
|
+
async function wrapFakeTmuxWithDetachedLeader(fakeTmuxPath) {
|
|
77
|
+
const implementationPath = `${fakeTmuxPath}.impl`;
|
|
78
|
+
await rename(fakeTmuxPath, implementationPath);
|
|
79
|
+
await writeExecutable(fakeTmuxPath, `#!/bin/sh
|
|
80
|
+
if [ "$1" = "display-message" ] && [ "$2" = "-p" ] && [ "$3" = "-t" ] && case "$5" in *'#{session_created}'*'#{window_id}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
81
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
82
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
83
|
+
printf '%s\t$1\t1\t0\t@1\t%s\t%s\n' "$session" "$4" "$leader_pid"
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
if [ "$1" = "set-option" ] && [ "$2" = "-t" ] && [ "$4" = "@omx_instance_id" ]; then
|
|
87
|
+
printf '%s' "$5" > /tmp/omx-test-detached-owner-id
|
|
88
|
+
fi
|
|
89
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{session_created}'*'#{@omx_instance_id}'*) true;; *) false;; esac; then
|
|
90
|
+
${JSON.stringify(implementationPath)} "$@" >/dev/null 2>&1 || true
|
|
91
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
92
|
+
owner=$(cat /tmp/omx-test-detached-owner-id 2>/dev/null || printf '')
|
|
93
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
94
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
95
|
+
printf '%s\t0\t%s\t%s\t$1\t1\t%s\n' "$pane" "$leader_pid" "$session" "$owner"
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
if [ "$1" = "if-shell" ] && [ "$2" = "-F" ] && [ "$3" = "-t" ]; then
|
|
99
|
+
if [ -f "$0.detached-recycled" ] && [ "$4" = "%99" ]; then ${JSON.stringify(implementationPath)} __nested_hud_guard_denied__ %99 999 '$2' @2; exit 0; fi
|
|
100
|
+
${JSON.stringify(implementationPath)} "$@" >/dev/null 2>&1 || true
|
|
101
|
+
success="$6"
|
|
102
|
+
receipt=$(printf '%s' "$success" | sed -n 's/.*\\(omx_detached_[A-Za-z0-9-]*\\).*/\\1/p')
|
|
103
|
+
case "\${OMX_TEST_DETACHED_RECYCLE:-}" in
|
|
104
|
+
split) case "$success" in *split-window*) exit 0;; esac ;;
|
|
105
|
+
finalization) case "$*" in
|
|
106
|
+
*split-window*) : > "$0.detached-split-seen" ;;
|
|
107
|
+
*"if-shell -F -t '%99'"*) if [ -f "$0.detached-split-seen" ]; then
|
|
108
|
+
${JSON.stringify(implementationPath)} __recycled__ %99 999 '$2' @2
|
|
109
|
+
: > "$0.detached-recycled"
|
|
110
|
+
eval "set -- $success"
|
|
111
|
+
"$0" "$@" >/dev/null
|
|
112
|
+
fi ;;
|
|
113
|
+
esac ;;
|
|
114
|
+
esac
|
|
115
|
+
[ -n "$receipt" ] || exit 1
|
|
116
|
+
case "$success" in
|
|
117
|
+
*split-window*) printf '%%99\t456\t$1\t@1\t%s\n' "$receipt" ;;
|
|
118
|
+
*) printf '%s\n' "$receipt" ;;
|
|
119
|
+
esac
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
123
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf '')
|
|
124
|
+
[ -n "$leader_pid" ] || exit 1
|
|
125
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
126
|
+
printf '%s\t0\t%s\n' "$pane" "$leader_pid"
|
|
127
|
+
exit 0
|
|
128
|
+
fi
|
|
129
|
+
output=$(${JSON.stringify(implementationPath)} "$@")
|
|
130
|
+
status=$?
|
|
131
|
+
printf '%s' "$output"
|
|
132
|
+
if [ "$status" -eq 0 ] && [ "$1" = "new-session" ]; then
|
|
133
|
+
previous=''
|
|
134
|
+
for arg in "$@"; do
|
|
135
|
+
if [ "$previous" = '-s' ]; then printf '%s' "$arg" > /tmp/omx-test-detached-session-name; break; fi
|
|
136
|
+
previous="$arg"
|
|
137
|
+
done
|
|
138
|
+
for last_arg do :; done
|
|
139
|
+
pane=$(printf '%s' "$output" | sed -n '1p')
|
|
140
|
+
TMUX=/tmp/omx-test-tmux,1,0 TMUX_PANE="$pane" nohup /bin/sh -c 'eval "exec $1"' sh "$last_arg" </dev/null >/tmp/omx-test-detached-leader.log 2>&1 &
|
|
141
|
+
leader_pid=$!
|
|
142
|
+
printf '%s' "$leader_pid" > "$0.detached-leader-pid"
|
|
143
|
+
printf '%s' "$pane" > "$0.detached-leader-pane"
|
|
144
|
+
(while kill -0 "$leader_pid" 2>/dev/null; do sleep 0.02; done; printf done > ${JSON.stringify(`${fakeTmuxPath}.leader-done`)}) </dev/null >/dev/null 2>&1 &
|
|
145
|
+
fi
|
|
146
|
+
exit "$status"
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
71
149
|
async function createLaunchFixture(wd, tmuxScript) {
|
|
72
150
|
const home = join(wd, 'home');
|
|
73
151
|
const fakeBin = join(wd, 'bin');
|
|
74
152
|
const tmuxLogPath = join(wd, 'tmux.log');
|
|
153
|
+
const leaderDonePath = join(wd, 'leader-done');
|
|
75
154
|
await mkdir(home, { recursive: true });
|
|
76
155
|
await mkdir(fakeBin, { recursive: true });
|
|
77
|
-
await writeExecutable(join(fakeBin, 'codex'), '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\n');
|
|
156
|
+
await writeExecutable(join(fakeBin, 'codex'), '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\nsleep 1\n');
|
|
78
157
|
await writeExecutable(join(fakeBin, 'ps'), '#!/bin/sh\nexit 0\n');
|
|
79
|
-
|
|
158
|
+
const tmuxImpl = join(fakeBin, 'tmux-impl');
|
|
159
|
+
await writeExecutable(tmuxImpl, tmuxScript(tmuxLogPath));
|
|
160
|
+
await writeExecutable(join(fakeBin, 'tmux'), `#!/bin/sh
|
|
161
|
+
if [ "$1" = "display-message" ] && [ "$2" = "-p" ] && [ "$3" = "-t" ] && case "$5" in *'#{session_created}'*'#{window_id}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
162
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
163
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
164
|
+
printf '%s\t$1\t1\t0\t@1\t%s\t%s\n' "$session" "$4" "$leader_pid"
|
|
165
|
+
exit 0
|
|
166
|
+
fi
|
|
167
|
+
if [ "$1" = "set-option" ] && [ "$2" = "-t" ] && [ "$4" = "@omx_instance_id" ]; then
|
|
168
|
+
printf '%s' "$5" > /tmp/omx-test-detached-owner-id
|
|
169
|
+
fi
|
|
170
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{session_created}'*'#{@omx_instance_id}'*) true;; *) false;; esac; then
|
|
171
|
+
${JSON.stringify(tmuxImpl)} "$@" >/dev/null 2>&1 || true
|
|
172
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
173
|
+
owner=$(cat /tmp/omx-test-detached-owner-id 2>/dev/null || printf '')
|
|
174
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
175
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
176
|
+
printf '%s\t0\t%s\t%s\t$1\t1\t%s\n' "$pane" "$leader_pid" "$session" "$owner"
|
|
177
|
+
exit 0
|
|
178
|
+
fi
|
|
179
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
180
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf '')
|
|
181
|
+
[ -n "$leader_pid" ] || exit 1
|
|
182
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
183
|
+
printf '%s\t0\t%s\n' "$pane" "$leader_pid"
|
|
184
|
+
exit 0
|
|
185
|
+
fi
|
|
186
|
+
if [ "$1" = "if-shell" ] && [ "$2" = "-F" ] && [ "$3" = "-t" ]; then
|
|
187
|
+
if [ -f "$0.detached-recycled" ] && [ "$4" = "%99" ]; then ${JSON.stringify(tmuxImpl)} __nested_hud_guard_denied__ %99 999 '$2' @2; exit 0; fi
|
|
188
|
+
${JSON.stringify(tmuxImpl)} "$@" >/dev/null 2>&1 || true
|
|
189
|
+
success="$6"
|
|
190
|
+
receipt=$(printf '%s' "$success" | sed -n 's/.*\\(omx_detached_[A-Za-z0-9-]*\\).*/\\1/p')
|
|
191
|
+
case "\${OMX_TEST_DETACHED_RECYCLE:-}" in
|
|
192
|
+
split) case "$success" in *split-window*) exit 0;; esac ;;
|
|
193
|
+
finalization) case "$*" in
|
|
194
|
+
*split-window*) : > "$0.detached-split-seen" ;;
|
|
195
|
+
*"if-shell -F -t '%99'"*) if [ -f "$0.detached-split-seen" ]; then
|
|
196
|
+
${JSON.stringify(tmuxImpl)} __recycled__ %99 999 '$2' @2
|
|
197
|
+
: > "$0.detached-recycled"
|
|
198
|
+
eval "set -- $success"
|
|
199
|
+
"$0" "$@" >/dev/null
|
|
200
|
+
fi ;;
|
|
201
|
+
esac ;;
|
|
202
|
+
esac
|
|
203
|
+
[ -n "$receipt" ] || exit 1
|
|
204
|
+
case "$success" in
|
|
205
|
+
*split-window*) printf '%%99\t456\t$1\t@1\t%s\n' "$receipt" ;;
|
|
206
|
+
*) printf '%s\n' "$receipt" ;;
|
|
207
|
+
esac
|
|
208
|
+
exit 0
|
|
209
|
+
fi
|
|
210
|
+
output=$(${JSON.stringify(tmuxImpl)} "$@")
|
|
211
|
+
status=$?
|
|
212
|
+
printf '%s' "$output"
|
|
213
|
+
if [ "$status" -eq 0 ] && [ "$1" = "new-session" ]; then
|
|
214
|
+
previous=''
|
|
215
|
+
for arg in "$@"; do
|
|
216
|
+
if [ "$previous" = '-s' ]; then printf '%s' "$arg" > /tmp/omx-test-detached-session-name; break; fi
|
|
217
|
+
previous="$arg"
|
|
218
|
+
done
|
|
219
|
+
for last_arg do :; done
|
|
220
|
+
previous=''
|
|
221
|
+
for arg in "$@"; do
|
|
222
|
+
if [ "$previous" = '-e' ]; then export "$arg"; fi
|
|
223
|
+
previous="$arg"
|
|
224
|
+
done
|
|
225
|
+
pane=$(printf '%s' "$output" | sed -n '1p')
|
|
226
|
+
TMUX=/tmp/omx-test-tmux,1,0 TMUX_PANE="$pane" nohup /bin/sh -c 'eval "exec $1"' sh "$last_arg" </dev/null >/tmp/omx-test-detached-leader.log 2>&1 &
|
|
227
|
+
leader_pid=$!
|
|
228
|
+
printf '%s' "$leader_pid" > "$0.detached-leader-pid"
|
|
229
|
+
printf '%s' "$pane" > "$0.detached-leader-pane"
|
|
230
|
+
(while kill -0 "$leader_pid" 2>/dev/null; do sleep 0.02; done; printf done > ${JSON.stringify(join(wd, 'leader-done'))}) </dev/null >/dev/null 2>&1 &
|
|
231
|
+
fi
|
|
232
|
+
exit "$status"
|
|
233
|
+
`);
|
|
80
234
|
return {
|
|
81
235
|
tmuxLogPath,
|
|
236
|
+
leaderDonePath,
|
|
82
237
|
env: {
|
|
83
238
|
HOME: home,
|
|
84
239
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -93,6 +248,104 @@ async function createLaunchFixture(wd, tmuxScript) {
|
|
|
93
248
|
},
|
|
94
249
|
};
|
|
95
250
|
}
|
|
251
|
+
function startHeldOmx(cwd, envOverrides) {
|
|
252
|
+
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
253
|
+
const repoRoot = join(testDir, '..', '..', '..');
|
|
254
|
+
return spawn(process.execPath, [join(repoRoot, 'dist', 'cli', 'omx.js'), '--direct', '--version'], {
|
|
255
|
+
cwd,
|
|
256
|
+
env: buildRunOmxEnv(envOverrides),
|
|
257
|
+
stdio: 'inherit',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async function waitForPath(path, expectedLines = 1, timeoutMs = 6_000) {
|
|
261
|
+
const deadline = Date.now() + timeoutMs;
|
|
262
|
+
while (Date.now() < deadline) {
|
|
263
|
+
if (existsSync(path)) {
|
|
264
|
+
const contents = await readFile(path, 'utf-8').catch(() => '');
|
|
265
|
+
if (contents.trim().split('\n').filter(Boolean).length >= expectedLines)
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`timed out waiting for ${path}`);
|
|
271
|
+
}
|
|
272
|
+
function parseShimTmuxArgv(contents) {
|
|
273
|
+
return contents
|
|
274
|
+
.split('tmux argv:\n')
|
|
275
|
+
.slice(1)
|
|
276
|
+
.map((record) => record.split('\nend tmux argv')[0].split('\n').filter(Boolean));
|
|
277
|
+
}
|
|
278
|
+
async function assertShimLogQuiescent(path, quietPeriodMs, message) {
|
|
279
|
+
const before = await readFile(path, 'utf-8').catch(() => '');
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, quietPeriodMs));
|
|
281
|
+
assert.equal(await readFile(path, 'utf-8').catch(() => ''), before, message);
|
|
282
|
+
}
|
|
283
|
+
async function waitForServerLogSubstring(readServerLog, offset, needle, timeoutMs = 3_000, intervalMs = 25) {
|
|
284
|
+
const deadline = Date.now() + timeoutMs;
|
|
285
|
+
let lastWindow = '';
|
|
286
|
+
while (Date.now() <= deadline) {
|
|
287
|
+
const log = await readServerLog();
|
|
288
|
+
lastWindow = log.slice(offset);
|
|
289
|
+
if (lastWindow.includes(needle))
|
|
290
|
+
return lastWindow;
|
|
291
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
292
|
+
}
|
|
293
|
+
throw new Error(`timed out after ${timeoutMs}ms waiting for ${JSON.stringify(needle)} in private tmux server log; tail=${JSON.stringify(lastWindow.slice(-2_000))}`);
|
|
294
|
+
}
|
|
295
|
+
async function stopHeldOmx(child, releasePath) {
|
|
296
|
+
await rm(releasePath, { force: true });
|
|
297
|
+
await new Promise((resolve, reject) => {
|
|
298
|
+
child.once('error', reject);
|
|
299
|
+
child.once('exit', () => resolve());
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async function createHeldCodexFixture(wd) {
|
|
303
|
+
const home = join(wd, 'home');
|
|
304
|
+
const fakeBin = join(wd, 'bin');
|
|
305
|
+
const releasePath = join(wd, 'hold');
|
|
306
|
+
const rootsPath = join(wd, 'roots.log');
|
|
307
|
+
await mkdir(home, { recursive: true });
|
|
308
|
+
await mkdir(fakeBin, { recursive: true });
|
|
309
|
+
await writeFile(releasePath, 'hold\n');
|
|
310
|
+
await writeExecutable(join(fakeBin, 'codex'), `#!/bin/sh
|
|
311
|
+
printf '%s\\n' "$OMX_ROOT" >> "${rootsPath}"
|
|
312
|
+
while [ -f "${releasePath}" ]; do sleep 1; done
|
|
313
|
+
`);
|
|
314
|
+
await writeExecutable(join(fakeBin, 'ps'), '#!/bin/sh\nexit 0\n');
|
|
315
|
+
return {
|
|
316
|
+
releasePath,
|
|
317
|
+
rootsPath,
|
|
318
|
+
env: {
|
|
319
|
+
HOME: home,
|
|
320
|
+
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
321
|
+
OMX_AUTO_UPDATE: '0',
|
|
322
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
323
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
324
|
+
OMX_ROOT: '',
|
|
325
|
+
OMX_STATE_ROOT: '',
|
|
326
|
+
TMUX: '',
|
|
327
|
+
TMUX_PANE: '',
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function skipUnlessPrivateRealTmux(t) {
|
|
332
|
+
if (isRealTmuxAvailable())
|
|
333
|
+
return true;
|
|
334
|
+
assert.equal(process.env.CI, undefined, 'CI must provide tmux for the private-server detached launch regression');
|
|
335
|
+
t.skip('tmux is not installed');
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
function skipUnlessScriptAvailable(t) {
|
|
339
|
+
if (process.platform === 'win32') {
|
|
340
|
+
t.skip('PTY launch harness requires posix script(1)');
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (isRealScriptAvailable())
|
|
344
|
+
return true;
|
|
345
|
+
assert.equal(process.env.CI, undefined, 'CI must provide script(1) for the PTY launch regression');
|
|
346
|
+
t.skip('script(1) is not installed');
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
96
349
|
describe('omx launch fallback when tmux is unavailable', () => {
|
|
97
350
|
it('surfaces direct Codex startup stderr and preserves the child exit code', async () => {
|
|
98
351
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-child-error-'));
|
|
@@ -122,7 +375,7 @@ exit 42
|
|
|
122
375
|
assert.match(result.stderr, /\[omx\] codex exited with code 42/);
|
|
123
376
|
}
|
|
124
377
|
finally {
|
|
125
|
-
await rm(wd, { recursive: true, force: true });
|
|
378
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
126
379
|
}
|
|
127
380
|
});
|
|
128
381
|
it('reports a missing Codex executable instead of exiting silently', async () => {
|
|
@@ -149,7 +402,7 @@ exit 42
|
|
|
149
402
|
assert.notEqual(result.stderr.trim(), '');
|
|
150
403
|
}
|
|
151
404
|
finally {
|
|
152
|
-
await rm(wd, { recursive: true, force: true });
|
|
405
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
153
406
|
}
|
|
154
407
|
});
|
|
155
408
|
it('launches codex directly without tmux ENOENT noise', async () => {
|
|
@@ -180,7 +433,256 @@ exit 42
|
|
|
180
433
|
assert.doesNotMatch(result.stderr, /spawnSync tmux ENOENT/);
|
|
181
434
|
}
|
|
182
435
|
finally {
|
|
183
|
-
await rm(wd, { recursive: true, force: true });
|
|
436
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
it('passes literal max and ultra after -- with raw config args to Codex launch unchanged', async () => {
|
|
440
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-reasoning-passthrough-'));
|
|
441
|
+
try {
|
|
442
|
+
const home = join(wd, 'home');
|
|
443
|
+
const fakeBin = join(wd, 'bin');
|
|
444
|
+
const fakeCodexPath = join(fakeBin, 'codex');
|
|
445
|
+
const fakePsPath = join(fakeBin, 'ps');
|
|
446
|
+
const fakeCapturePath = join(wd, 'fake-codex-argv.json');
|
|
447
|
+
await mkdir(home, { recursive: true });
|
|
448
|
+
await mkdir(fakeBin, { recursive: true });
|
|
449
|
+
await writeFile(fakeCodexPath, [
|
|
450
|
+
'#!/bin/sh',
|
|
451
|
+
`exec "$NODE_BINARY" -e 'require("node:fs").writeFileSync(process.env.OMX_FAKE_CODEX_CAPTURE_PATH, JSON.stringify(process.argv.slice(1)))' -- "$@"`,
|
|
452
|
+
'',
|
|
453
|
+
].join('\n'));
|
|
454
|
+
await chmod(fakeCodexPath, 0o755);
|
|
455
|
+
await writeFile(fakePsPath, '#!/bin/sh\nexit 0\n');
|
|
456
|
+
await chmod(fakePsPath, 0o755);
|
|
457
|
+
const result = runOmx(wd, [
|
|
458
|
+
'--direct',
|
|
459
|
+
'-c',
|
|
460
|
+
'model_reasoning_effort=MAX',
|
|
461
|
+
'--',
|
|
462
|
+
'--max',
|
|
463
|
+
'--ultra',
|
|
464
|
+
'-c',
|
|
465
|
+
'model_reasoning_effort="ultra"',
|
|
466
|
+
'-c',
|
|
467
|
+
'model_reasoning_effort=future',
|
|
468
|
+
'suffix argument with spaces',
|
|
469
|
+
'',
|
|
470
|
+
'--',
|
|
471
|
+
'after second marker',
|
|
472
|
+
], {
|
|
473
|
+
HOME: home,
|
|
474
|
+
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
475
|
+
NODE_OPTIONS: '',
|
|
476
|
+
OMX_AUTO_UPDATE: '0',
|
|
477
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
478
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
479
|
+
NODE_BINARY: process.execPath,
|
|
480
|
+
OMX_FAKE_CODEX_CAPTURE_PATH: fakeCapturePath,
|
|
481
|
+
TMUX: '',
|
|
482
|
+
TMUX_PANE: '',
|
|
483
|
+
});
|
|
484
|
+
if (shouldSkipForSpawnPermissions(result.error))
|
|
485
|
+
return;
|
|
486
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
487
|
+
const capturedArgv = JSON.parse(await readFile(fakeCapturePath, 'utf-8'));
|
|
488
|
+
const firstMarkerIndex = capturedArgv.indexOf('--');
|
|
489
|
+
assert.equal(firstMarkerIndex, 4);
|
|
490
|
+
const modelInstructionsArg = capturedArgv[firstMarkerIndex - 1];
|
|
491
|
+
assert.match(modelInstructionsArg, /^model_instructions_file="[^"\n]+"$/);
|
|
492
|
+
assert.match(modelInstructionsArg, /\.omx\/state\/sessions\/omx-[^"]+\/AGENTS\.md"$/);
|
|
493
|
+
assert.equal(capturedArgv.filter((arg) => arg.startsWith('model_instructions_file=')).length, 1);
|
|
494
|
+
assert.deepEqual(capturedArgv, [
|
|
495
|
+
'-c',
|
|
496
|
+
'model_reasoning_effort=MAX',
|
|
497
|
+
'-c',
|
|
498
|
+
modelInstructionsArg,
|
|
499
|
+
'--',
|
|
500
|
+
'--max',
|
|
501
|
+
'--ultra',
|
|
502
|
+
'-c',
|
|
503
|
+
'model_reasoning_effort="ultra"',
|
|
504
|
+
'-c',
|
|
505
|
+
'model_reasoning_effort=future',
|
|
506
|
+
'suffix argument with spaces',
|
|
507
|
+
'',
|
|
508
|
+
'--',
|
|
509
|
+
'after second marker',
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
it('treats image values as variadic when detecting resume launches', async () => {
|
|
517
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-image-resume-'));
|
|
518
|
+
try {
|
|
519
|
+
const home = join(wd, 'home');
|
|
520
|
+
const fakeBin = join(wd, 'bin');
|
|
521
|
+
const fakeCapturePath = join(wd, 'fake-codex.json');
|
|
522
|
+
await mkdir(join(wd, '.omx'), { recursive: true });
|
|
523
|
+
await mkdir(join(wd, '.codex'), { recursive: true });
|
|
524
|
+
await mkdir(home, { recursive: true });
|
|
525
|
+
await mkdir(fakeBin, { recursive: true });
|
|
526
|
+
await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
|
|
527
|
+
await writeFile(join(wd, '.codex', 'state_5.sqlite'), 'resume-only-sentinel');
|
|
528
|
+
await writeExecutable(join(fakeBin, 'codex'), `#!/bin/sh
|
|
529
|
+
exec "$NODE_BINARY" -e 'const fs = require("node:fs"); const path = require("node:path"); fs.writeFileSync(process.env.OMX_FAKE_CODEX_CAPTURE_PATH, JSON.stringify({ argv: process.argv.slice(1), hasResumeSqlite: fs.existsSync(path.join(process.env.CODEX_HOME, "state_5.sqlite")) }))' -- "$@"
|
|
530
|
+
`);
|
|
531
|
+
const cases = [
|
|
532
|
+
{ args: ['-i', 'resume'], resumes: false },
|
|
533
|
+
{ args: ['--image', 'resume'], resumes: false },
|
|
534
|
+
{ args: ['--image=resume'], resumes: false },
|
|
535
|
+
{ args: ['-iresume'], resumes: false },
|
|
536
|
+
{ args: ['-i=resume'], resumes: false },
|
|
537
|
+
{ args: ['-i', 'screenshot.png', 'resume'], resumes: false },
|
|
538
|
+
{ args: ['--image', 'screenshot.png', 'resume'], resumes: false },
|
|
539
|
+
{ args: ['--image=screenshot.png', 'resume'], resumes: true },
|
|
540
|
+
{ args: ['-iscreenshot.png', 'resume'], resumes: true },
|
|
541
|
+
{ args: ['-i=screenshot.png', 'resume'], resumes: true },
|
|
542
|
+
{ args: ['-i', 'one.png', '-i', 'two.png', 'resume'], resumes: false },
|
|
543
|
+
{ args: ['--image', 'one.png', '--image', 'two.png', 'resume'], resumes: false },
|
|
544
|
+
{ args: ['-i', 'one.png,two.png', 'resume'], resumes: false },
|
|
545
|
+
{ args: ['--image', 'one.png,two.png', 'resume'], resumes: false },
|
|
546
|
+
{ args: ['--image', 'one.png', '--model', 'gpt-review', 'resume'], resumes: true },
|
|
547
|
+
{ args: ['--image=one.png', '--model=gpt-review', 'resume'], resumes: true },
|
|
548
|
+
];
|
|
549
|
+
for (const testCase of cases) {
|
|
550
|
+
const result = runOmx(wd, ['--direct', ...testCase.args], {
|
|
551
|
+
HOME: home,
|
|
552
|
+
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
553
|
+
NODE_BINARY: process.execPath,
|
|
554
|
+
OMX_AUTO_UPDATE: '0',
|
|
555
|
+
OMX_BYPASS_DEFAULT_SYSTEM_PROMPT: '0',
|
|
556
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
557
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
558
|
+
OMX_FAKE_CODEX_CAPTURE_PATH: fakeCapturePath,
|
|
559
|
+
});
|
|
560
|
+
if (shouldSkipForSpawnPermissions(result.error))
|
|
561
|
+
return;
|
|
562
|
+
assert.equal(result.status, 0, `${JSON.stringify(testCase.args)}: ${result.error || result.stderr || result.stdout}`);
|
|
563
|
+
const captured = JSON.parse(await readFile(fakeCapturePath, 'utf-8'));
|
|
564
|
+
assert.deepEqual(captured.argv, testCase.args);
|
|
565
|
+
assert.equal(captured.hasResumeSqlite, testCase.resumes, JSON.stringify(testCase.args));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
describe('ordinary launch root collision guidance', () => {
|
|
574
|
+
it('keeps the cwd default for the first launch and fails the second and third launches closed with explicit-root guidance', async () => {
|
|
575
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-conflict-'));
|
|
576
|
+
let binding;
|
|
577
|
+
try {
|
|
578
|
+
execFileSync('git', ['init'], { cwd: wd, stdio: 'ignore' });
|
|
579
|
+
const fixture = await createHeldCodexFixture(wd);
|
|
580
|
+
const established = await establishLaunchSessionBinding(wd, 'first-standard-launch', { pid: process.pid });
|
|
581
|
+
assert.equal(established.kind, 'committed-released');
|
|
582
|
+
if (established.kind !== 'committed-released')
|
|
583
|
+
return;
|
|
584
|
+
binding = established.binding;
|
|
585
|
+
const second = runOmx(wd, ['--direct', '--version'], fixture.env);
|
|
586
|
+
const third = runOmx(wd, ['--direct', '--version'], fixture.env);
|
|
587
|
+
if (shouldSkipForSpawnPermissions(second.error) || shouldSkipForSpawnPermissions(third.error))
|
|
588
|
+
return;
|
|
589
|
+
for (const result of [second, third]) {
|
|
590
|
+
assert.notEqual(result.status, 0, result.stderr || result.stdout);
|
|
591
|
+
assert.match(result.stderr, /session_pointer_owner_conflict/);
|
|
592
|
+
assert.match(result.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
|
|
593
|
+
assert.match(result.stderr, /POSIX: OMX_ROOT="\$HOME\/\.omx\/instances\/second-conversation" omx/);
|
|
594
|
+
assert.match(result.stderr, /PowerShell: \$env:OMX_ROOT = "\$HOME\/\.omx\/instances\/second-conversation"; omx/);
|
|
595
|
+
assert.match(result.stderr, /cmd\.exe: set "OMX_ROOT=%USERPROFILE%\\\.omx\\instances\\second-conversation" && omx/);
|
|
596
|
+
assert.match(result.stderr, /OMX does not reroute or allocate one automatically/);
|
|
597
|
+
}
|
|
598
|
+
const resume = runOmx(wd, ['--direct', 'resume'], fixture.env);
|
|
599
|
+
if (!shouldSkipForSpawnPermissions(resume.error)) {
|
|
600
|
+
assert.notEqual(resume.status, 0, resume.stderr || resume.stdout);
|
|
601
|
+
assert.match(resume.stderr, /session_pointer_owner_conflict/);
|
|
602
|
+
assert.doesNotMatch(resume.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
|
|
603
|
+
}
|
|
604
|
+
await finalizeBoundOnce(binding, 'test');
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
if (binding)
|
|
608
|
+
await closeLaunchSessionBindingOnce(binding).catch(() => { });
|
|
609
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
it('keeps explicit roots literal: distinct roots launch independently while a shared root remains fatal without reroute guidance', async () => {
|
|
613
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-explicit-roots-'));
|
|
614
|
+
execFileSync('git', ['init'], { cwd: wd, stdio: 'ignore' });
|
|
615
|
+
let first;
|
|
616
|
+
let second;
|
|
617
|
+
try {
|
|
618
|
+
const fixture = await createHeldCodexFixture(wd);
|
|
619
|
+
const firstRoot = join(wd, 'first-root');
|
|
620
|
+
const secondRoot = join(wd, 'second-root');
|
|
621
|
+
first = startHeldOmx(wd, { ...fixture.env, OMX_ROOT: firstRoot });
|
|
622
|
+
second = startHeldOmx(wd, { ...fixture.env, OMX_ROOT: secondRoot });
|
|
623
|
+
await waitForPath(fixture.rootsPath, 2);
|
|
624
|
+
assert.deepEqual(new Set((await readFile(fixture.rootsPath, 'utf-8')).trim().split('\n')), new Set([firstRoot, secondRoot]));
|
|
625
|
+
const collision = runOmx(wd, ['--direct', '--version'], { ...fixture.env, OMX_ROOT: firstRoot });
|
|
626
|
+
if (shouldSkipForSpawnPermissions(collision.error))
|
|
627
|
+
return;
|
|
628
|
+
assert.notEqual(collision.status, 0, collision.stderr || collision.stdout);
|
|
629
|
+
assert.match(collision.stderr, /session_pointer_owner_conflict/);
|
|
630
|
+
assert.doesNotMatch(collision.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
|
|
631
|
+
assert.doesNotMatch(collision.stderr, /reroute or allocate one automatically/);
|
|
632
|
+
await stopHeldOmx(first, fixture.releasePath);
|
|
633
|
+
first = undefined;
|
|
634
|
+
if (second.exitCode === null) {
|
|
635
|
+
await new Promise((resolve) => second.once('exit', () => resolve()));
|
|
636
|
+
}
|
|
637
|
+
second = undefined;
|
|
638
|
+
}
|
|
639
|
+
finally {
|
|
640
|
+
if (first)
|
|
641
|
+
first.kill('SIGKILL');
|
|
642
|
+
if (second)
|
|
643
|
+
second.kill('SIGKILL');
|
|
644
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
it('keeps different checkout defaults independent and fails closed on stale default-pointer evidence', async () => {
|
|
648
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-stale-'));
|
|
649
|
+
let firstBinding;
|
|
650
|
+
let secondBinding;
|
|
651
|
+
try {
|
|
652
|
+
const firstCheckout = join(wd, 'first-checkout');
|
|
653
|
+
const secondCheckout = join(wd, 'second-checkout');
|
|
654
|
+
await mkdir(firstCheckout, { recursive: true });
|
|
655
|
+
await mkdir(secondCheckout, { recursive: true });
|
|
656
|
+
execFileSync('git', ['init'], { cwd: firstCheckout, stdio: 'ignore' });
|
|
657
|
+
execFileSync('git', ['init'], { cwd: secondCheckout, stdio: 'ignore' });
|
|
658
|
+
const firstEstablished = await establishLaunchSessionBinding(firstCheckout, 'first-checkout-owner', { pid: process.pid });
|
|
659
|
+
const secondEstablished = await establishLaunchSessionBinding(secondCheckout, 'second-checkout-owner', { pid: process.pid });
|
|
660
|
+
assert.equal(firstEstablished.kind, 'committed-released');
|
|
661
|
+
assert.equal(secondEstablished.kind, 'committed-released');
|
|
662
|
+
if (firstEstablished.kind !== 'committed-released' || secondEstablished.kind !== 'committed-released')
|
|
663
|
+
return;
|
|
664
|
+
firstBinding = firstEstablished.binding;
|
|
665
|
+
secondBinding = secondEstablished.binding;
|
|
666
|
+
assert.match(await readFile(join(firstCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /first-checkout-owner/);
|
|
667
|
+
assert.match(await readFile(join(secondCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /second-checkout-owner/);
|
|
668
|
+
await finalizeBoundOnce(firstBinding, 'test');
|
|
669
|
+
await finalizeBoundOnce(secondBinding, 'test');
|
|
670
|
+
await writeSessionStart(firstCheckout, 'stale-owner', { pid: 2_147_483_647 });
|
|
671
|
+
const fixture = await createHeldCodexFixture(wd);
|
|
672
|
+
await rm(fixture.releasePath, { force: true });
|
|
673
|
+
const relaunch = runOmx(firstCheckout, ['--direct', '--version'], fixture.env);
|
|
674
|
+
if (shouldSkipForSpawnPermissions(relaunch.error))
|
|
675
|
+
return;
|
|
676
|
+
assert.notEqual(relaunch.status, 0, relaunch.error || relaunch.stderr || relaunch.stdout);
|
|
677
|
+
assert.match(relaunch.stderr, /session_pointer_unusable|stale-dead/);
|
|
678
|
+
assert.match(await readFile(join(firstCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /stale-owner/);
|
|
679
|
+
}
|
|
680
|
+
finally {
|
|
681
|
+
if (firstBinding)
|
|
682
|
+
await closeLaunchSessionBindingOnce(firstBinding).catch(() => { });
|
|
683
|
+
if (secondBinding)
|
|
684
|
+
await closeLaunchSessionBindingOnce(secondBinding).catch(() => { });
|
|
685
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
184
686
|
}
|
|
185
687
|
});
|
|
186
688
|
});
|
|
@@ -218,7 +720,7 @@ printf 'fake-codex:%s\n' "$*"
|
|
|
218
720
|
assert.equal(existsSync(join(worktreePath, '.omx')), false);
|
|
219
721
|
}
|
|
220
722
|
finally {
|
|
221
|
-
await rm(wd, { recursive: true, force: true });
|
|
723
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
222
724
|
}
|
|
223
725
|
});
|
|
224
726
|
it('preserves explicit OMX_ROOT for launch worktree state', async () => {
|
|
@@ -254,7 +756,7 @@ printf 'fake-codex:%s\n' "$*"
|
|
|
254
756
|
assert.equal(existsSync(join(repo, '.omx')), false);
|
|
255
757
|
}
|
|
256
758
|
finally {
|
|
257
|
-
await rm(wd, { recursive: true, force: true });
|
|
759
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
258
760
|
}
|
|
259
761
|
});
|
|
260
762
|
it('keeps direct madmax worktree launches bound to the boxed run root', async () => {
|
|
@@ -304,7 +806,7 @@ printf 'fake-codex:%s\n' "$*"
|
|
|
304
806
|
assert.match(normalizedStdout, /fake-codex-context:[0-9a-f]{32}/);
|
|
305
807
|
}
|
|
306
808
|
finally {
|
|
307
|
-
await rm(wd, { recursive: true, force: true });
|
|
809
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
308
810
|
}
|
|
309
811
|
});
|
|
310
812
|
});
|
|
@@ -363,20 +865,406 @@ exit 0
|
|
|
363
865
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
364
866
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
365
867
|
assert.match(tmuxLog, /tmux:new-session /);
|
|
366
|
-
assert.match(tmuxLog, /tmux:split-window
|
|
868
|
+
assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*split-window/);
|
|
367
869
|
assert.doesNotMatch(tmuxLog, /tmux:attach-session/);
|
|
368
870
|
assert.doesNotMatch(result.stderr, /failed to attach detached tmux session/);
|
|
369
871
|
}
|
|
370
872
|
finally {
|
|
371
|
-
await rm(wd, { recursive: true, force: true });
|
|
873
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
372
874
|
}
|
|
373
875
|
});
|
|
374
876
|
});
|
|
375
877
|
describe('omx launcher when tmux is available', () => {
|
|
376
|
-
it('
|
|
878
|
+
it('restores an enlarged detached HUD on the first larger client attach', async (t) => {
|
|
879
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
880
|
+
return;
|
|
881
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
|
|
882
|
+
try {
|
|
883
|
+
await withTempTmuxSession(async (fixture) => {
|
|
884
|
+
const home = join(wd, 'home');
|
|
885
|
+
const bin = join(wd, 'bin');
|
|
886
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
887
|
+
await mkdir(home, { recursive: true });
|
|
888
|
+
await mkdir(bin, { recursive: true });
|
|
889
|
+
await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
|
|
890
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
891
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
892
|
+
HOME: home,
|
|
893
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
894
|
+
OMX_AUTO_UPDATE: '0',
|
|
895
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
896
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
897
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
898
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
899
|
+
TMUX: '',
|
|
900
|
+
TMUX_PANE: '',
|
|
901
|
+
});
|
|
902
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
903
|
+
const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
904
|
+
.split('\n')
|
|
905
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
906
|
+
assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
|
|
907
|
+
// tmux 3.2a does not expose session_width/session_height; the detached window is the session geometry.
|
|
908
|
+
assert.equal(fixture.run(['display-message', '-p', '-t', launchedSession, '#{window_width}x#{window_height}']), '80x24', 'the detached session must remain at its initial geometry before any client attaches');
|
|
909
|
+
const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
|
|
910
|
+
.split('\n')
|
|
911
|
+
.map((line) => line.split('\t'));
|
|
912
|
+
const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
|
|
913
|
+
assert.ok(hud, 'HUD split must carry its operation marker');
|
|
914
|
+
assert.equal(Number(hud[1]), HUD_TMUX_HEIGHT_LINES, 'authorized direct reconciliation must resize the HUD');
|
|
915
|
+
assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, 'history-limit']), String(DETACHED_TMUX_HISTORY_LIMIT), 'direct leader mutation must receive its receipt and set session history');
|
|
916
|
+
const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
917
|
+
assert.match(hooks, /client-resized\[[0-9]+\].*run-shell -b/);
|
|
918
|
+
assert.match(hooks, /tmux if-shell -F -t/);
|
|
919
|
+
assert.match(hooks, /OMX_DETACHED_HUD_OPERATION=/);
|
|
920
|
+
const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
|
|
921
|
+
assert.equal(storedPaneFormat, '##{pane_id}\\t##{pane_dead}\\t##{pane_pid}', 'stored hook display must retain one outer format escape and encode TAB bytes');
|
|
922
|
+
const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
|
|
923
|
+
assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
|
|
924
|
+
const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
925
|
+
for (const hookSlot of clientAttachedHookSlots) {
|
|
926
|
+
fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
|
|
927
|
+
}
|
|
928
|
+
const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
929
|
+
const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
930
|
+
assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
|
|
931
|
+
assert.match(installedHooks, /client-resized\[[0-9]+\].*run-shell -b/);
|
|
932
|
+
assert.doesNotMatch(installedHooks, /client-attached\[[0-9]+\]/, 'the resize trigger must not be conflated with client-attached hook execution');
|
|
933
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
934
|
+
const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
935
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
936
|
+
assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
|
|
937
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
|
|
938
|
+
await writeFile(shimLogPath, '');
|
|
939
|
+
assert.equal(await readFile(shimLogPath, 'utf-8'), '', 'the shim baseline must start after launch and reconciliation activity settles');
|
|
940
|
+
fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
|
|
941
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the restore trigger');
|
|
942
|
+
assert.equal(fixture.run(['display-message', '-p', '-t', launchedSession, '#{window_width}x#{window_height}']), '80x24', 'enlarging the detached HUD must not attach a client');
|
|
943
|
+
fixture.triggerClientResize(launchedSession, { cols: 121, rows: 41 });
|
|
944
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
945
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
|
|
946
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), HUD_TMUX_HEIGHT_LINES, 'the first larger client attach must restore the enlarged detached HUD');
|
|
947
|
+
const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
948
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
949
|
+
assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
|
|
950
|
+
for (const argv of paneSnapshots) {
|
|
951
|
+
assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
finally {
|
|
956
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
it('records the guarded resize command without an unknown-command parser error', async (t) => {
|
|
960
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
961
|
+
return;
|
|
962
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
|
|
963
|
+
try {
|
|
964
|
+
await withTempTmuxSession({ serverLog: true }, async (fixture) => {
|
|
965
|
+
const home = join(wd, 'home');
|
|
966
|
+
const bin = join(wd, 'bin');
|
|
967
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
968
|
+
await mkdir(home, { recursive: true });
|
|
969
|
+
await mkdir(bin, { recursive: true });
|
|
970
|
+
await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
|
|
971
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
972
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
973
|
+
HOME: home,
|
|
974
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
975
|
+
OMX_AUTO_UPDATE: '0',
|
|
976
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
977
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
978
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
979
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
980
|
+
TMUX: '',
|
|
981
|
+
TMUX_PANE: '',
|
|
982
|
+
});
|
|
983
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
984
|
+
const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
985
|
+
.split('\n')
|
|
986
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
987
|
+
assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
|
|
988
|
+
const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
|
|
989
|
+
.split('\n')
|
|
990
|
+
.map((line) => line.split('\t'));
|
|
991
|
+
const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
|
|
992
|
+
assert.ok(hud, 'HUD split must carry its operation marker');
|
|
993
|
+
const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
994
|
+
const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
|
|
995
|
+
const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
|
|
996
|
+
assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
|
|
997
|
+
const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
998
|
+
for (const hookSlot of clientAttachedHookSlots) {
|
|
999
|
+
fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
|
|
1000
|
+
}
|
|
1001
|
+
const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
1002
|
+
const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
1003
|
+
assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
|
|
1004
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1005
|
+
const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1006
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1007
|
+
assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
|
|
1008
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
|
|
1009
|
+
await writeFile(shimLogPath, '');
|
|
1010
|
+
const probeOffset = (await fixture.readServerLog()).length;
|
|
1011
|
+
const probe = fixture.runResult([
|
|
1012
|
+
'if-shell', '-F', '-t', fixture.leaderPaneId, '1',
|
|
1013
|
+
'tmux display-message -p omx-oracle-probe', '',
|
|
1014
|
+
]);
|
|
1015
|
+
assert.equal(probe.error, '', 'the positive-control tmux client must start successfully');
|
|
1016
|
+
assert.notEqual(probe.status, 0, 'tmux must reject the deliberately malformed selected command list');
|
|
1017
|
+
assert.ok(probe.stderr === '' || /unknown command: tmux/.test(probe.stderr), `accepted probe stderr is empty or the same parse diagnostic, got ${JSON.stringify(probe.stderr)}`);
|
|
1018
|
+
assert.equal(fixture.sessionExists(), true, 'the private tmux server must remain alive after the malformed client command');
|
|
1019
|
+
assert.equal(fixture.run(['display-message', '-p', '-t', fixture.leaderPaneId, '#{pane_id}']), fixture.leaderPaneId, 'a successful post-probe command must prove the server remains responsive');
|
|
1020
|
+
await waitForServerLogSubstring(fixture.readServerLog, probeOffset, 'unknown command: tmux', 3_000, 25);
|
|
1021
|
+
const triggerOffset = (await fixture.readServerLog()).length;
|
|
1022
|
+
fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
|
|
1023
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the parser oracle trigger');
|
|
1024
|
+
fixture.triggerClientResize(launchedSession, { cols: 121, rows: 41 });
|
|
1025
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1026
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
|
|
1027
|
+
const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1028
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1029
|
+
assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
|
|
1030
|
+
for (const argv of paneSnapshots) {
|
|
1031
|
+
assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
|
|
1032
|
+
}
|
|
1033
|
+
const triggerWindow = (await fixture.readServerLog()).slice(triggerOffset);
|
|
1034
|
+
assert.ok(triggerWindow.includes(`resize-pane -t ${hud[0]} -y ${HUD_TMUX_HEIGHT_LINES}`), 'the server-log window must cover the stored hook resize command');
|
|
1035
|
+
assert.doesNotMatch(triggerWindow, /unknown command: tmux/, 'the triggered guarded sink must parse as a bare tmux command-list entry (#3292)');
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
finally {
|
|
1039
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
it('denies detached HUD mutation after the pane owner changes', async (t) => {
|
|
1043
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
1044
|
+
return;
|
|
1045
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
|
|
1046
|
+
try {
|
|
1047
|
+
await withTempTmuxSession(async (fixture) => {
|
|
1048
|
+
const home = join(wd, 'home');
|
|
1049
|
+
const bin = join(wd, 'bin');
|
|
1050
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
1051
|
+
const foreignSession = 'foreign-control';
|
|
1052
|
+
let launchedSession;
|
|
1053
|
+
let originalOwner;
|
|
1054
|
+
await mkdir(home, { recursive: true });
|
|
1055
|
+
await mkdir(bin, { recursive: true });
|
|
1056
|
+
await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
|
|
1057
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
1058
|
+
fixture.run(['new-session', '-d', '-s', foreignSession, 'sleep 300']);
|
|
1059
|
+
const foreignBefore = fixture.run(['list-panes', '-t', foreignSession, '-F', '#{pane_id}\t#{pane_pid}\t#{pane_height}']);
|
|
1060
|
+
try {
|
|
1061
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1062
|
+
HOME: home,
|
|
1063
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
1064
|
+
OMX_AUTO_UPDATE: '0',
|
|
1065
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
1066
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
1067
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
1068
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
1069
|
+
TMUX: '',
|
|
1070
|
+
TMUX_PANE: '',
|
|
1071
|
+
});
|
|
1072
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1073
|
+
launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
1074
|
+
.split('\n')
|
|
1075
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
1076
|
+
assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
|
|
1077
|
+
const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
|
|
1078
|
+
.split('\n')
|
|
1079
|
+
.map((line) => line.split('\t'));
|
|
1080
|
+
const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
|
|
1081
|
+
assert.ok(hud, 'HUD split must carry its operation marker');
|
|
1082
|
+
const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
1083
|
+
const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
|
|
1084
|
+
const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
|
|
1085
|
+
assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
|
|
1086
|
+
const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
1087
|
+
for (const hookSlot of clientAttachedHookSlots) {
|
|
1088
|
+
fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
|
|
1089
|
+
}
|
|
1090
|
+
const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
1091
|
+
const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
1092
|
+
assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
|
|
1093
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1094
|
+
const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1095
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1096
|
+
assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
|
|
1097
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
|
|
1098
|
+
await writeFile(shimLogPath, '');
|
|
1099
|
+
originalOwner = fixture.run(['show-options', '-v', '-t', launchedSession, '@omx_instance_id']);
|
|
1100
|
+
assert.ok(originalOwner, 'the detached session must retain its original pane owner before the denial trigger');
|
|
1101
|
+
fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
|
|
1102
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the authority denial trigger');
|
|
1103
|
+
fixture.run(['set-option', '-t', launchedSession, '@omx_instance_id', 'foreign-owner']);
|
|
1104
|
+
await writeFile(shimLogPath, '');
|
|
1105
|
+
fixture.triggerClientResize(launchedSession, { cols: 122, rows: 42 });
|
|
1106
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1107
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
|
|
1108
|
+
const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1109
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1110
|
+
assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
|
|
1111
|
+
for (const argv of paneSnapshots) {
|
|
1112
|
+
assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
|
|
1113
|
+
}
|
|
1114
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the foreign pane owner must deny the deferred HUD mutation');
|
|
1115
|
+
assert.equal(fixture.run(['list-panes', '-t', foreignSession, '-F', '#{pane_id}\t#{pane_pid}\t#{pane_height}']), foreignBefore, 'the denial trigger must not affect a foreign session');
|
|
1116
|
+
}
|
|
1117
|
+
finally {
|
|
1118
|
+
if (launchedSession && originalOwner && fixture.sessionExists(launchedSession)) {
|
|
1119
|
+
fixture.run(['set-option', '-t', launchedSession, '@omx_instance_id', originalOwner]);
|
|
1120
|
+
assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, '@omx_instance_id']), originalOwner, 'the original detached pane owner must be restored before fixture teardown');
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
finally {
|
|
1126
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
it('cleans a poisoned private tmux server before replaying a held child and Stop', async (t) => {
|
|
1130
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
1131
|
+
return;
|
|
1132
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-poisoned-server-'));
|
|
1133
|
+
try {
|
|
1134
|
+
await withTempTmuxSession(async (fixture) => {
|
|
1135
|
+
const home = join(wd, 'home');
|
|
1136
|
+
const bin = join(wd, 'bin');
|
|
1137
|
+
const releasePath = join(wd, 'child-release');
|
|
1138
|
+
const capturePath = join(wd, 'child-b-env.log');
|
|
1139
|
+
const poisonRuns = join(wd, 'poison-runs');
|
|
1140
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
1141
|
+
const poisonEnv = Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [
|
|
1142
|
+
key,
|
|
1143
|
+
key === 'OMX_RUNS_DIR' ? poisonRuns : `poison-${key.toLowerCase()}`,
|
|
1144
|
+
]));
|
|
1145
|
+
await mkdir(home, { recursive: true });
|
|
1146
|
+
await mkdir(bin, { recursive: true });
|
|
1147
|
+
await writeFile(releasePath, 'hold\n');
|
|
1148
|
+
const captureLines = DETACHED_LAUNCH_CONTROL_PLANE_KEYS
|
|
1149
|
+
.map((key) => `printf '%s=%s\\n' '${key}' "$${key}"`)
|
|
1150
|
+
.join('\n');
|
|
1151
|
+
await writeExecutable(join(bin, 'codex'), `#!/bin/sh
|
|
1152
|
+
{
|
|
1153
|
+
${captureLines}
|
|
1154
|
+
} > ${JSON.stringify(capturePath)}
|
|
1155
|
+
while [ -f ${JSON.stringify(releasePath)} ]; do sleep 1; done
|
|
1156
|
+
`);
|
|
1157
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
1158
|
+
for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
|
|
1159
|
+
fixture.run(['set-environment', '-g', key, poisonEnv[key]]);
|
|
1160
|
+
}
|
|
1161
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1162
|
+
...poisonEnv,
|
|
1163
|
+
HOME: home,
|
|
1164
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
1165
|
+
OMX_AUTO_UPDATE: '0',
|
|
1166
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
1167
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
1168
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
1169
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
1170
|
+
TMUX: '',
|
|
1171
|
+
TMUX_PANE: '',
|
|
1172
|
+
});
|
|
1173
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1174
|
+
assert.doesNotMatch(`${result.stderr}\n${result.stdout}`, /scope|pointer|state root.*error/i);
|
|
1175
|
+
await waitForPath(capturePath, 1, 15_000);
|
|
1176
|
+
const captured = Object.fromEntries((await readFile(capturePath, 'utf-8')).trim().split('\n').map((line) => {
|
|
1177
|
+
const separator = line.indexOf('=');
|
|
1178
|
+
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
1179
|
+
}));
|
|
1180
|
+
assert.deepEqual(Object.keys(captured), [...DETACHED_LAUNCH_CONTROL_PLANE_KEYS]);
|
|
1181
|
+
assert.match(captured.OMX_SESSION_ID ?? '', /^omx-/);
|
|
1182
|
+
assert.equal(captured.CODEX_SESSION_ID, '');
|
|
1183
|
+
assert.equal(captured.SESSION_ID, '');
|
|
1184
|
+
assert.equal(captured.OMX_TMUX_HUD_OWNER, '1');
|
|
1185
|
+
assert.equal(captured.OMX_TMUX_HUD_LEADER_PANE, '');
|
|
1186
|
+
assert.match(captured.OMX_ROOT ?? '', new RegExp(`^${escapeRegExp(poisonRuns)}/run-`));
|
|
1187
|
+
assert.equal(captured.OMX_STATE_ROOT, '');
|
|
1188
|
+
assert.equal(captured.OMX_TEAM_STATE_ROOT, '');
|
|
1189
|
+
assert.equal(captured.OMXBOX_ACTIVE, '1');
|
|
1190
|
+
assert.equal(captured.OMX_SOURCE_CWD, wd);
|
|
1191
|
+
assert.match(captured.OMX_MADMAX_DETACHED_CONTEXT ?? '', /^[0-9a-f]{32}$/);
|
|
1192
|
+
assert.equal(captured.OMX_RUNS_DIR, poisonRuns);
|
|
1193
|
+
assert.equal(captured.OMX_TEAM_LEADER_CWD, '');
|
|
1194
|
+
assert.equal(captured.OMX_TEAM_WORKER, '');
|
|
1195
|
+
assert.equal(captured.OMX_TEAM_INTERNAL_WORKER, '');
|
|
1196
|
+
const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
1197
|
+
.split('\n')
|
|
1198
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
1199
|
+
assert.ok(launchedSession, 'compiled launcher must create a detached session on the private server');
|
|
1200
|
+
assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, 'history-limit']), String(DETACHED_TMUX_HISTORY_LIMIT));
|
|
1201
|
+
const sessionJsonPath = join(captured.OMX_ROOT, '.omx', 'state', 'session.json');
|
|
1202
|
+
await waitForPath(sessionJsonPath);
|
|
1203
|
+
const sessionJsonBeforeStop = await readFile(sessionJsonPath, 'utf-8');
|
|
1204
|
+
const sessionStateBeforeStop = JSON.parse(sessionJsonBeforeStop);
|
|
1205
|
+
assert.equal(sessionStateBeforeStop.session_id, captured.OMX_SESSION_ID);
|
|
1206
|
+
assert.equal(sessionStateBeforeStop.tmux_session_name, launchedSession);
|
|
1207
|
+
assert.match(sessionStateBeforeStop.tmux_pane_id ?? '', /^%[0-9]+$/);
|
|
1208
|
+
const stop = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'scripts', 'codex-native-hook.js')], {
|
|
1209
|
+
cwd: wd,
|
|
1210
|
+
input: JSON.stringify({
|
|
1211
|
+
hook_event_name: 'Stop',
|
|
1212
|
+
source: 'codex-app',
|
|
1213
|
+
cwd: wd,
|
|
1214
|
+
session_id: captured.OMX_SESSION_ID,
|
|
1215
|
+
thread_id: 'poisoned-server-child-b',
|
|
1216
|
+
turn_id: 'stop-poisoned-server-child-b',
|
|
1217
|
+
}),
|
|
1218
|
+
encoding: 'utf-8',
|
|
1219
|
+
env: buildRunOmxEnv({
|
|
1220
|
+
...captured,
|
|
1221
|
+
HOME: home,
|
|
1222
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
1223
|
+
OMX_AUTO_UPDATE: '0',
|
|
1224
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
1225
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
1226
|
+
TMUX: '',
|
|
1227
|
+
TMUX_PANE: '',
|
|
1228
|
+
}),
|
|
1229
|
+
});
|
|
1230
|
+
assert.equal(stop.status, 0, stop.stderr || stop.stdout);
|
|
1231
|
+
assert.doesNotMatch(`${stop.stderr}\n${stop.stdout}`, /scope|pointer|state root.*error/i);
|
|
1232
|
+
const stopOutput = JSON.parse(stop.stdout.trim());
|
|
1233
|
+
assert.deepEqual(stopOutput, {}, 'a clean held-child Stop must be a JSON no-op');
|
|
1234
|
+
assert.notEqual(stopOutput.decision, 'block');
|
|
1235
|
+
const sessionJsonAfterStop = await readFile(sessionJsonPath, 'utf-8');
|
|
1236
|
+
const sessionStateAfterStop = JSON.parse(sessionJsonAfterStop);
|
|
1237
|
+
assert.equal(sessionStateAfterStop.session_id, captured.OMX_SESSION_ID);
|
|
1238
|
+
assert.equal(sessionStateAfterStop.tmux_session_name, launchedSession);
|
|
1239
|
+
assert.equal(sessionJsonAfterStop, sessionJsonBeforeStop, 'a no-op Stop must preserve the live B session pointer');
|
|
1240
|
+
const activeRecordPath = join(captured.OMX_RUNS_DIR, 'active-detached', `${captured.OMX_MADMAX_DETACHED_CONTEXT}.json`);
|
|
1241
|
+
assert.equal(existsSync(activeRecordPath), true, 'leader must retain its active record while child B is live');
|
|
1242
|
+
await rm(releasePath, { force: true });
|
|
1243
|
+
for (let attempt = 0; attempt < 300 && fixture.sessionExists(launchedSession); attempt += 1) {
|
|
1244
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1245
|
+
}
|
|
1246
|
+
assert.equal(fixture.sessionExists(launchedSession), false, 'released child must clean up its detached tmux session');
|
|
1247
|
+
await waitForPath(join(captured.OMX_ROOT, '.omx', 'logs', 'session-history.jsonl'));
|
|
1248
|
+
assert.equal(existsSync(activeRecordPath), false, 'released child must remove its active record');
|
|
1249
|
+
assert.match(await readFile(join(captured.OMX_ROOT, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'), new RegExp(escapeRegExp(captured.OMX_SESSION_ID)));
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
finally {
|
|
1253
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
it('does not let the outer launcher fabricate leader-owned active records', async () => {
|
|
377
1257
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-reuse-'));
|
|
378
1258
|
try {
|
|
379
1259
|
const runs = join(wd, 'runs');
|
|
1260
|
+
const madmaxRoot = join(wd, 'madmax-root');
|
|
1261
|
+
const madmaxContext = buildMadmaxDetachedLaunchContextKey(wd, ['--madmax', '--tmux'], madmaxRoot);
|
|
1262
|
+
await mkdir(madmaxRoot, { recursive: true });
|
|
1263
|
+
await writeFile(join(madmaxRoot, '.omxbox-run.json'), JSON.stringify({
|
|
1264
|
+
cwd: madmaxRoot,
|
|
1265
|
+
source_cwd: wd,
|
|
1266
|
+
detached_launch_context: madmaxContext,
|
|
1267
|
+
}));
|
|
380
1268
|
const activeMarker = join(wd, 'active-session');
|
|
381
1269
|
const instanceMarker = join(wd, 'active-instance');
|
|
382
1270
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
@@ -403,6 +1291,12 @@ case "$1" in
|
|
|
403
1291
|
printf 'hud-pane\n'
|
|
404
1292
|
exit 0
|
|
405
1293
|
;;
|
|
1294
|
+
list-panes)
|
|
1295
|
+
session=$(cat "${activeMarker}")
|
|
1296
|
+
instance=$(cat "${instanceMarker}")
|
|
1297
|
+
printf '%%12\t0\t4242\t%s\t$1\t100\t%s\n' "$session" "$instance"
|
|
1298
|
+
exit 0
|
|
1299
|
+
;;
|
|
406
1300
|
display-message)
|
|
407
1301
|
if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
|
|
408
1302
|
printf '/tmp/tmux-test.sock\n'
|
|
@@ -438,8 +1332,10 @@ exit 0
|
|
|
438
1332
|
const baseEnv = {
|
|
439
1333
|
...env,
|
|
440
1334
|
OMX_RUNS_DIR: runs,
|
|
1335
|
+
OMX_ROOT: madmaxRoot,
|
|
441
1336
|
OMXBOX_ACTIVE: '1',
|
|
442
|
-
|
|
1337
|
+
OMX_SOURCE_CWD: wd,
|
|
1338
|
+
OMX_MADMAX_DETACHED_CONTEXT: madmaxContext,
|
|
443
1339
|
OMX_LAUNCH_POLICY: 'direct',
|
|
444
1340
|
TMUX: '',
|
|
445
1341
|
TMUX_PANE: '',
|
|
@@ -452,27 +1348,34 @@ exit 0
|
|
|
452
1348
|
if (shouldSkipForSpawnPermissions(second.error))
|
|
453
1349
|
return;
|
|
454
1350
|
assert.equal(second.status, 0, second.error || second.stderr || second.stdout);
|
|
455
|
-
assert.match(second.stderr, /madmax detached launch already active for this context
|
|
1351
|
+
assert.match(second.stderr, /madmax detached launch already active for this context/);
|
|
456
1352
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1353
|
+
assert.match(tmuxLog, new RegExp(`-e OMX_MADMAX_DETACHED_CONTEXT=${escapeRegExp(madmaxContext)}`));
|
|
1354
|
+
assert.match(tmuxLog, new RegExp(`-e OMX_RUNS_DIR=${escapeRegExp(runs)}`));
|
|
457
1355
|
assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 1);
|
|
458
|
-
assert.equal((tmuxLog.match(/tmux:has-session/g) || []).length,
|
|
1356
|
+
assert.equal((tmuxLog.match(/tmux:has-session/g) || []).length, 0);
|
|
1357
|
+
assert.ok((tmuxLog.match(/tmux:list-panes/g) || []).length >= 2);
|
|
459
1358
|
assert.equal((tmuxLog.match(/tmux:attach-session/g) || []).length, 2);
|
|
460
|
-
const
|
|
461
|
-
assert.match(
|
|
462
|
-
assert.match(
|
|
463
|
-
assert.match(
|
|
1359
|
+
const activeRecord = await readFile(join(runs, 'active-detached', `${madmaxContext}.json`), 'utf-8');
|
|
1360
|
+
assert.match(activeRecord, /"tmux_session_name"/);
|
|
1361
|
+
assert.match(activeRecord, /"session_id"/);
|
|
1362
|
+
assert.match(activeRecord, /"tmux_pane_id": "%12"/);
|
|
1363
|
+
assert.match(activeRecord, /"leader_pid"/);
|
|
1364
|
+
assert.match(activeRecord, /"base_state_root"/);
|
|
1365
|
+
assert.match(activeRecord, /"lifecycle_phase": "ready"/);
|
|
464
1366
|
}
|
|
465
1367
|
finally {
|
|
466
|
-
await rm(wd, { recursive: true, force: true });
|
|
1368
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
467
1369
|
}
|
|
468
1370
|
});
|
|
469
|
-
it('records boxed runtime identity
|
|
1371
|
+
it('records boxed runtime identity in the leader-owned active record', async () => {
|
|
470
1372
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-worktree-detached-'));
|
|
471
1373
|
try {
|
|
472
1374
|
const repo = await createGitRepo(wd);
|
|
473
1375
|
const runs = join(wd, 'runs');
|
|
474
1376
|
const instanceMarker = join(wd, 'active-instance');
|
|
475
|
-
const
|
|
1377
|
+
const sessionMarker = join(wd, 'active-session-name');
|
|
1378
|
+
const { env, tmuxLogPath, leaderDonePath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
476
1379
|
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
477
1380
|
case "$1" in
|
|
478
1381
|
-V)
|
|
@@ -483,13 +1386,25 @@ case "$1" in
|
|
|
483
1386
|
exit 1
|
|
484
1387
|
;;
|
|
485
1388
|
new-session)
|
|
1389
|
+
prev=''
|
|
1390
|
+
for arg in "$@"; do
|
|
1391
|
+
if [ "$prev" = '-s' ]; then printf '%s' "$arg" > "${sessionMarker}"; fi
|
|
1392
|
+
prev="$arg"
|
|
1393
|
+
done
|
|
486
1394
|
printf '%%77\n'
|
|
487
1395
|
exit 0
|
|
488
1396
|
;;
|
|
1397
|
+
|
|
489
1398
|
split-window)
|
|
490
1399
|
printf '%%78\n'
|
|
491
1400
|
exit 0
|
|
492
1401
|
;;
|
|
1402
|
+
list-panes)
|
|
1403
|
+
instance=$(cat "${instanceMarker}")
|
|
1404
|
+
printf '%%77\t0\t4242\t%s\t$1\t100\t%s\n' "$(cat "${sessionMarker}")" "$instance"
|
|
1405
|
+
exit 0
|
|
1406
|
+
;;
|
|
1407
|
+
|
|
493
1408
|
display-message)
|
|
494
1409
|
if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
|
|
495
1410
|
printf '/tmp/tmux-test.sock\n'
|
|
@@ -535,19 +1450,17 @@ exit 0
|
|
|
535
1450
|
assert.equal(activeFiles.length, 1);
|
|
536
1451
|
const activeRecord = JSON.parse(await readFile(join(runs, 'active-detached', activeFiles[0]), 'utf-8'));
|
|
537
1452
|
const worktreePath = join(dirname(repo), `${basename(repo)}.omx-worktrees`, 'launch-detached');
|
|
538
|
-
assert.
|
|
539
|
-
assert.equal(
|
|
540
|
-
assert.equal(
|
|
541
|
-
assert.equal(activeRecord.session_id.startsWith('omx-'), true);
|
|
542
|
-
assert.equal(activeRecord.tmux_pane_id, '%77');
|
|
1453
|
+
assert.equal(normalizeDarwinTmpPath(activeRecord.source_cwd), normalizeDarwinTmpPath(worktreePath));
|
|
1454
|
+
assert.equal(typeof activeRecord.leader_pid, 'number');
|
|
1455
|
+
assert.equal(activeRecord.lifecycle_phase, 'ready');
|
|
543
1456
|
const tmuxLog = normalizeDarwinTmpPath(await readFile(tmuxLogPath, 'utf-8'));
|
|
544
|
-
assert.match(tmuxLog,
|
|
1457
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
545
1458
|
assert.match(tmuxLog, /-e OMXBOX_ACTIVE=1/);
|
|
546
1459
|
assert.match(tmuxLog, new RegExp(`-e OMX_SOURCE_CWD=${escapeRegExp(normalizeDarwinTmpPath(repo))}`));
|
|
547
|
-
|
|
1460
|
+
await waitForPath(leaderDonePath);
|
|
548
1461
|
}
|
|
549
1462
|
finally {
|
|
550
|
-
await rm(wd, { recursive: true, force: true });
|
|
1463
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
551
1464
|
}
|
|
552
1465
|
});
|
|
553
1466
|
it('does not mutate a stale active-detached tmux session without OMX ownership proof', async () => {
|
|
@@ -567,7 +1480,7 @@ exit 0
|
|
|
567
1480
|
session_id: 'expected-omx-session-id',
|
|
568
1481
|
tmux_pane_id: '%99',
|
|
569
1482
|
})}\n`);
|
|
570
|
-
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
1483
|
+
const { env, tmuxLogPath, leaderDonePath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
571
1484
|
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
572
1485
|
case "$1" in
|
|
573
1486
|
-V)
|
|
@@ -621,29 +1534,35 @@ exit 0
|
|
|
621
1534
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
622
1535
|
return;
|
|
623
1536
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1537
|
+
await waitForPath(leaderDonePath);
|
|
624
1538
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
625
1539
|
assert.doesNotMatch(tmuxLog, /tmux:set-option .* -t user-owned-session .*history-limit/);
|
|
626
1540
|
assert.doesNotMatch(tmuxLog, /tmux:clear-history .*user-owned-session|tmux:clear-history .*%99/);
|
|
627
1541
|
assert.match(tmuxLog, /tmux:new-session /);
|
|
628
1542
|
}
|
|
629
1543
|
finally {
|
|
630
|
-
await rm(wd, { recursive: true, force: true });
|
|
1544
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
631
1545
|
}
|
|
632
1546
|
});
|
|
633
1547
|
it('does not reuse the same active-detached lock for independent --madmax --high launches', async () => {
|
|
634
1548
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-independent-high-'));
|
|
635
1549
|
try {
|
|
636
1550
|
const runs = join(wd, 'runs');
|
|
1551
|
+
const sessionMarker = join(wd, 'independent-session');
|
|
1552
|
+
const instanceMarker = join(wd, 'independent-instance');
|
|
637
1553
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
638
1554
|
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
639
1555
|
case "$1" in
|
|
640
1556
|
-V) printf 'tmux 3.4\n'; exit 0 ;;
|
|
641
1557
|
has-session) exit 1 ;;
|
|
642
|
-
new-session) printf '%%12\n'; exit 0 ;;
|
|
1558
|
+
new-session) prev=''; for arg in "$@"; do if [ "$prev" = '-s' ]; then printf '%s' "$arg" > "${sessionMarker}"; fi; prev="$arg"; done; printf '%%12\n'; exit 0 ;;
|
|
1559
|
+
|
|
643
1560
|
split-window) printf 'hud-pane\n'; exit 0 ;;
|
|
644
1561
|
display-message) if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then printf '/tmp/tmux-test.sock\n'; else printf '0\n'; fi; exit 0 ;;
|
|
1562
|
+
list-panes) printf '%%12\t0\t4242\t%s\t$1\t100\t%s\n' "$(cat "${sessionMarker}")" "$(cat "${instanceMarker}")"; exit 0 ;;
|
|
645
1563
|
show-options) printf 'off\n'; exit 0 ;;
|
|
646
|
-
set-option
|
|
1564
|
+
set-option) if [ "$4" = '@omx_instance_id' ]; then printf '%s' "$5" > "${instanceMarker}"; fi; exit 0 ;;
|
|
1565
|
+
set-hook|attach-session|kill-session|run-shell|resize-pane) exit 0 ;;
|
|
647
1566
|
esac
|
|
648
1567
|
exit 0
|
|
649
1568
|
`);
|
|
@@ -668,13 +1587,14 @@ exit 0
|
|
|
668
1587
|
.map((line) => JSON.parse(line));
|
|
669
1588
|
assert.equal(registryEntries.length, 2);
|
|
670
1589
|
assert.notEqual(registryEntries[0].detached_launch_context, registryEntries[1].detached_launch_context, 'independent launches must get distinct active-detached lock identities');
|
|
671
|
-
|
|
672
|
-
assert.equal(
|
|
1590
|
+
const activeDetachedEntries = await readdir(join(runs, 'active-detached'));
|
|
1591
|
+
assert.equal(activeDetachedEntries.filter((entry) => entry.endsWith('.json')).length, 2);
|
|
1592
|
+
assert.equal(activeDetachedEntries.filter((entry) => entry.endsWith('.lock')).length, 0);
|
|
673
1593
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
674
1594
|
assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
|
|
675
1595
|
}
|
|
676
1596
|
finally {
|
|
677
|
-
await rm(wd, { recursive: true, force: true });
|
|
1597
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
678
1598
|
}
|
|
679
1599
|
});
|
|
680
1600
|
it('allows distinct madmax detached launch contexts to create separate sessions', async () => {
|
|
@@ -711,7 +1631,44 @@ exit 0
|
|
|
711
1631
|
assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
|
|
712
1632
|
}
|
|
713
1633
|
finally {
|
|
714
|
-
await rm(wd, { recursive: true, force: true });
|
|
1634
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
it('denies detached HUD finalization when the receipted pane id is recycled', async () => {
|
|
1638
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-detached-hud-recycle-'));
|
|
1639
|
+
try {
|
|
1640
|
+
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
1641
|
+
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
1642
|
+
case "$1" in
|
|
1643
|
+
-V) printf 'tmux 3.4\n'; exit 0 ;;
|
|
1644
|
+
has-session) exit 1 ;;
|
|
1645
|
+
new-session) printf '%%12\n'; exit 0 ;;
|
|
1646
|
+
split-window) printf '%%99\n'; exit 0 ;;
|
|
1647
|
+
display-message) printf '0\n'; exit 0 ;;
|
|
1648
|
+
show-options) printf 'off\n'; exit 0 ;;
|
|
1649
|
+
set-option|set-hook|attach-session|kill-session|run-shell|resize-pane) exit 0 ;;
|
|
1650
|
+
esac
|
|
1651
|
+
exit 0
|
|
1652
|
+
`);
|
|
1653
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1654
|
+
...env,
|
|
1655
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
1656
|
+
OMX_TEST_DETACHED_RECYCLE: 'finalization',
|
|
1657
|
+
TMUX: '',
|
|
1658
|
+
TMUX_PANE: '',
|
|
1659
|
+
});
|
|
1660
|
+
if (shouldSkipForSpawnPermissions(result.error))
|
|
1661
|
+
return;
|
|
1662
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1663
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1664
|
+
assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*OMX_DETACHED_HUD_OPERATION=/);
|
|
1665
|
+
assert.match(tmuxLog, /if-shell -F -t '%99' .*456.*\$1.*@1.*OMX_DETACHED_HUD_OPERATION=/);
|
|
1666
|
+
assert.match(tmuxLog, /^tmux:__nested_hud_guard_denied__ %99 999 \$2 @2$/m);
|
|
1667
|
+
assert.doesNotMatch(tmuxLog, /run-shell -b ['"]run-shell -b /);
|
|
1668
|
+
assert.doesNotMatch(tmuxLog, /^tmux:resize-pane .*%99/m);
|
|
1669
|
+
}
|
|
1670
|
+
finally {
|
|
1671
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
715
1672
|
}
|
|
716
1673
|
});
|
|
717
1674
|
it('launches --madmax through explicitly requested detached tmux so HUD bootstrap can run', async () => {
|
|
@@ -763,6 +1720,7 @@ esac
|
|
|
763
1720
|
exit 0
|
|
764
1721
|
`);
|
|
765
1722
|
await chmod(fakeTmuxPath, 0o755);
|
|
1723
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
766
1724
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
767
1725
|
HOME: home,
|
|
768
1726
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -779,14 +1737,15 @@ exit 0
|
|
|
779
1737
|
assert.doesNotMatch(tmuxLog, /tmux:show-options -gv history-limit/);
|
|
780
1738
|
assert.doesNotMatch(tmuxLog, /tmux:set-option -g[q ]+history-limit/);
|
|
781
1739
|
assert.match(tmuxLog, /tmux:new-session .* -s /);
|
|
782
|
-
assert.match(tmuxLog, new RegExp(`tmux:
|
|
783
|
-
assert.match(tmuxLog, new RegExp(`tmux:
|
|
784
|
-
assert.match(tmuxLog, /tmux:
|
|
785
|
-
assert.match(tmuxLog, new RegExp(`tmux:
|
|
1740
|
+
assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*set-option.*history-limit.*${DETACHED_TMUX_HISTORY_LIMIT}`));
|
|
1741
|
+
assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*set-option.*-pq.*%12.*history-limit.*${DETACHED_TMUX_HISTORY_LIMIT}`));
|
|
1742
|
+
assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*set-hook.*client-detached\[[0-9]+\].*clear-history -t %12/);
|
|
1743
|
+
assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*split-window.*-v.*-l.*${HUD_TMUX_HEIGHT_LINES}.*-t.*%12`));
|
|
786
1744
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1745
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
787
1746
|
}
|
|
788
1747
|
finally {
|
|
789
|
-
await rm(wd, { recursive: true, force: true });
|
|
1748
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
790
1749
|
}
|
|
791
1750
|
});
|
|
792
1751
|
it('preserves parent provider env without replaying terminal state over an OMX-created tmux pane', async () => {
|
|
@@ -838,8 +1797,8 @@ case "$1" in
|
|
|
838
1797
|
TERMINFO=/tmp/server-terminfo \
|
|
839
1798
|
TERMINFO_DIRS=/tmp/server-terminfo-dirs \
|
|
840
1799
|
TERMCAP=server-termcap \
|
|
841
|
-
sh -c "$last"
|
|
842
|
-
printf '
|
|
1800
|
+
nohup /bin/sh -c "$last" </dev/null >/tmp/omx-test-provider-leader.log 2>&1 &
|
|
1801
|
+
printf '%%12\n'
|
|
843
1802
|
exit 0
|
|
844
1803
|
;;
|
|
845
1804
|
split-window)
|
|
@@ -864,6 +1823,7 @@ case "$1" in
|
|
|
864
1823
|
esac
|
|
865
1824
|
exit 0
|
|
866
1825
|
`);
|
|
1826
|
+
await wrapFakeTmuxWithDetachedLeader(join(fakeBin, 'tmux'));
|
|
867
1827
|
const result = runOmx(wd, ['--tmux', '--madmax'], {
|
|
868
1828
|
HOME: home,
|
|
869
1829
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -887,28 +1847,12 @@ exit 0
|
|
|
887
1847
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
888
1848
|
return;
|
|
889
1849
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
890
|
-
assert.equal(await readFile(envLogPath, 'utf-8'), [
|
|
891
|
-
'custom=fake-provider-key',
|
|
892
|
-
'marker=1',
|
|
893
|
-
'term=tmux-256color',
|
|
894
|
-
'term_program=tmux',
|
|
895
|
-
'term_program_version=3.4',
|
|
896
|
-
'colorterm=truecolor',
|
|
897
|
-
'tmux=/tmp/tmux-test.sock,123,0',
|
|
898
|
-
'tmux_pane=%12',
|
|
899
|
-
'columns=211',
|
|
900
|
-
'lines=77',
|
|
901
|
-
'terminfo=/tmp/outer-terminfo',
|
|
902
|
-
'terminfo_dirs=/tmp/outer-terminfo-dirs',
|
|
903
|
-
'termcap=outer-termcap',
|
|
904
|
-
'',
|
|
905
|
-
].join('\n'));
|
|
906
1850
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
907
|
-
assert.
|
|
908
|
-
assert.doesNotMatch(tmuxLog, /CUSTOM_LLM_API_KEY=/);
|
|
1851
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
1852
|
+
assert.doesNotMatch(tmuxLog, /fake-provider-key|CUSTOM_LLM_API_KEY=/);
|
|
909
1853
|
}
|
|
910
1854
|
finally {
|
|
911
|
-
await rm(wd, { recursive: true, force: true });
|
|
1855
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
912
1856
|
}
|
|
913
1857
|
});
|
|
914
1858
|
it('launches directly with --direct and skips detached tmux bootstrap', async () => {
|
|
@@ -936,7 +1880,7 @@ exit 0
|
|
|
936
1880
|
assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
|
|
937
1881
|
}
|
|
938
1882
|
finally {
|
|
939
|
-
await rm(wd, { recursive: true, force: true });
|
|
1883
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
940
1884
|
}
|
|
941
1885
|
});
|
|
942
1886
|
it('launches directly from OMX_LAUNCH_POLICY=direct and skips detached tmux bootstrap', async () => {
|
|
@@ -960,7 +1904,7 @@ exit 0
|
|
|
960
1904
|
assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
|
|
961
1905
|
}
|
|
962
1906
|
finally {
|
|
963
|
-
await rm(wd, { recursive: true, force: true });
|
|
1907
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
964
1908
|
}
|
|
965
1909
|
});
|
|
966
1910
|
it('launches directly inside tmux with --direct and skips HUD/mouse/extended-key tmux calls', async () => {
|
|
@@ -983,35 +1927,68 @@ exit 0
|
|
|
983
1927
|
assert.doesNotMatch(tmuxLog, /split-window|show-options|extended-keys|mouse on/);
|
|
984
1928
|
}
|
|
985
1929
|
finally {
|
|
986
|
-
await rm(wd, { recursive: true, force: true });
|
|
1930
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
987
1931
|
}
|
|
988
1932
|
});
|
|
989
|
-
it('
|
|
1933
|
+
it('fails closed on an incomplete synthetic HUD split source inside tmux', async () => {
|
|
990
1934
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-inside-tmux-managed-'));
|
|
991
1935
|
try {
|
|
992
1936
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (tmuxLogPath) => `#!/bin/sh
|
|
1937
|
+
state="${tmuxLogPath}.hud-created"
|
|
1938
|
+
proof="${tmuxLogPath}.hud-proof"
|
|
1939
|
+
marker="${tmuxLogPath}.hud-marker"
|
|
993
1940
|
printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
|
|
994
1941
|
case "$1" in
|
|
995
1942
|
list-panes)
|
|
1943
|
+
case "$*" in
|
|
1944
|
+
*'#{pane_id}'*'#{pane_start_command}'*)
|
|
1945
|
+
if [ -f "$state" ]; then printf '%%1\tcodex\n%%2\tOMX_TMUX_SPLIT_OPERATION_MARKER='\''%s'\''; export OMX_TMUX_SPLIT_OPERATION_MARKER; exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='\''%%1'\'' node hud\n' "$(cat "$marker")"; else printf '%%1\tcodex\n'; fi
|
|
1946
|
+
;;
|
|
1947
|
+
*'#{pane_id}\t#{pane_dead}\t#{pane_pid}'*)
|
|
1948
|
+
printf '%%1\t0\t101\n%%2\t0\t202\n'
|
|
1949
|
+
;;
|
|
1950
|
+
*'#{pane_id}'*)
|
|
1951
|
+
if [ -f "$state" ]; then printf '%%1\n%%2\n'; else printf '%%1\n'; fi
|
|
1952
|
+
;;
|
|
1953
|
+
esac
|
|
996
1954
|
exit 0
|
|
997
1955
|
;;
|
|
998
1956
|
split-window)
|
|
999
|
-
printf '
|
|
1957
|
+
printf '%%2\n'
|
|
1958
|
+
exit 0
|
|
1959
|
+
;;
|
|
1960
|
+
if-shell)
|
|
1961
|
+
[ "$2" = '-F' ] && [ "$3" = '-t' ] && [ "$4" = '%1' ] || exit 1
|
|
1962
|
+
case "$5" in *'#{pane_pid},101'*'#{session_id},$7'*'#{window_id},@1'*) ;; *) exit 1 ;; esac
|
|
1963
|
+
success="$6"
|
|
1964
|
+
receipt=\${success##*display-message -p }
|
|
1965
|
+
receipt=\${receipt%% *}
|
|
1966
|
+
split_marker=$(printf '%s' "$success" | sed -n "s/.*OMX_TMUX_SPLIT_OPERATION_MARKER='\\([^']*\\)'.*/\\1/p")
|
|
1967
|
+
[ -n "$split_marker" ] || exit 1
|
|
1968
|
+
printf '%s' "$split_marker" > "$marker"
|
|
1969
|
+
: > "$state"
|
|
1970
|
+
printf '%s\n' "$receipt"
|
|
1000
1971
|
exit 0
|
|
1001
1972
|
;;
|
|
1002
1973
|
display-message)
|
|
1003
1974
|
case "$*" in
|
|
1004
1975
|
*'#{socket_path}'*) printf '/tmp/tmux-test.sock\n' ;;
|
|
1976
|
+
*'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*'#{session_id}'*'#{window_id}'*) printf '%%1\t0\t101\t$7\t@1\n' ;;
|
|
1977
|
+
*'#{session_id}'*'#{window_id}'*) printf '$7\t@1\n' ;;
|
|
1005
1978
|
*'#S'*) printf 'managed-session\n' ;;
|
|
1006
1979
|
*) printf '0\n' ;;
|
|
1007
1980
|
esac
|
|
1008
1981
|
exit 0
|
|
1009
1982
|
;;
|
|
1010
1983
|
show-options)
|
|
1011
|
-
printf 'off\n'
|
|
1984
|
+
if [ "$2" = '-g' ] && [ "$3" = '-v' ]; then cat "$proof" 2>/dev/null || true; else printf 'off\n'; fi
|
|
1012
1985
|
exit 0
|
|
1013
1986
|
;;
|
|
1014
|
-
set-option
|
|
1987
|
+
set-option)
|
|
1988
|
+
if [ "$2" = '-g' ]; then printf '%s\n' "$4" > "$proof"; fi
|
|
1989
|
+
exit 0
|
|
1990
|
+
;;
|
|
1991
|
+
kill-pane)
|
|
1015
1992
|
exit 0
|
|
1016
1993
|
;;
|
|
1017
1994
|
esac
|
|
@@ -1027,15 +2004,15 @@ exit 0
|
|
|
1027
2004
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1028
2005
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1029
2006
|
assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
|
|
1030
|
-
assert.
|
|
2007
|
+
assert.doesNotMatch(tmuxLog, /tmux:if-shell -F -t %1 .*split-window/);
|
|
1031
2008
|
assert.match(tmuxLog, /tmux:set-option -t managed-session mouse on/);
|
|
1032
2009
|
assert.match(tmuxLog, /tmux:set-option -sq extended-keys always/);
|
|
1033
2010
|
}
|
|
1034
2011
|
finally {
|
|
1035
|
-
await rm(wd, { recursive: true, force: true });
|
|
2012
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1036
2013
|
}
|
|
1037
2014
|
});
|
|
1038
|
-
it('
|
|
2015
|
+
it('does not probe an existing tmux server before detached tmux startup', async () => {
|
|
1039
2016
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-missing-socket-'));
|
|
1040
2017
|
try {
|
|
1041
2018
|
const home = join(wd, 'home');
|
|
@@ -1057,12 +2034,9 @@ case "$1" in
|
|
|
1057
2034
|
printf 'tmux 3.6a\n'
|
|
1058
2035
|
exit 0
|
|
1059
2036
|
;;
|
|
1060
|
-
|
|
1061
|
-
printf 'error connecting to /private/tmp/tmux-501/default (No such file or directory)\n' >&2
|
|
1062
|
-
exit 1
|
|
1063
|
-
;;
|
|
2037
|
+
|
|
1064
2038
|
new-session)
|
|
1065
|
-
printf '
|
|
2039
|
+
printf '%%12\n'
|
|
1066
2040
|
exit 0
|
|
1067
2041
|
;;
|
|
1068
2042
|
split-window)
|
|
@@ -1088,6 +2062,7 @@ esac
|
|
|
1088
2062
|
exit 0
|
|
1089
2063
|
`);
|
|
1090
2064
|
await chmod(fakeTmuxPath, 0o755);
|
|
2065
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1091
2066
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1092
2067
|
HOME: home,
|
|
1093
2068
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -1101,15 +2076,15 @@ exit 0
|
|
|
1101
2076
|
return;
|
|
1102
2077
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1103
2078
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1104
|
-
assert.
|
|
2079
|
+
assert.doesNotMatch(tmuxLog, /tmux:list-sessions/);
|
|
1105
2080
|
assert.match(tmuxLog, /tmux:new-session .* -s /);
|
|
1106
2081
|
assert.doesNotMatch(result.stderr, /server\/socket is unusable/);
|
|
1107
2082
|
}
|
|
1108
2083
|
finally {
|
|
1109
|
-
await rm(wd, { recursive: true, force: true });
|
|
2084
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1110
2085
|
}
|
|
1111
2086
|
});
|
|
1112
|
-
it('
|
|
2087
|
+
it('does not fall back directly when a later detached tmux operation fails', async () => {
|
|
1113
2088
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-stale-socket-'));
|
|
1114
2089
|
try {
|
|
1115
2090
|
const home = join(wd, 'home');
|
|
@@ -1118,6 +2093,7 @@ exit 0
|
|
|
1118
2093
|
const fakePsPath = join(fakeBin, 'ps');
|
|
1119
2094
|
const fakeTmuxPath = join(fakeBin, 'tmux');
|
|
1120
2095
|
const tmuxLogPath = join(wd, 'tmux.log');
|
|
2096
|
+
const runsPath = join(wd, 'runs');
|
|
1121
2097
|
await mkdir(home, { recursive: true });
|
|
1122
2098
|
await mkdir(fakeBin, { recursive: true });
|
|
1123
2099
|
await writeFile(fakeCodexPath, '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\n');
|
|
@@ -1128,40 +2104,49 @@ exit 0
|
|
|
1128
2104
|
printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
|
|
1129
2105
|
case "$1" in
|
|
1130
2106
|
-V)
|
|
2107
|
+
printf 'tmux:d0-runs:%s\n' "$(test -e "${runsPath}" && printf present || printf missing)" >> "${tmuxLogPath}"
|
|
2108
|
+
|
|
1131
2109
|
printf 'tmux 3.6a\n'
|
|
1132
2110
|
exit 0
|
|
1133
2111
|
;;
|
|
1134
|
-
|
|
1135
|
-
printf '
|
|
2112
|
+
new-session)
|
|
2113
|
+
printf 'unsafe-tmux-handle-secret\n' >&2
|
|
1136
2114
|
exit 1
|
|
1137
2115
|
;;
|
|
2116
|
+
|
|
1138
2117
|
esac
|
|
1139
2118
|
printf 'unexpected tmux command: %s\n' "$*" >&2
|
|
1140
2119
|
exit 1
|
|
1141
2120
|
`);
|
|
1142
2121
|
await chmod(fakeTmuxPath, 0o755);
|
|
2122
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1143
2123
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1144
2124
|
HOME: home,
|
|
1145
2125
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
1146
2126
|
OMX_AUTO_UPDATE: '0',
|
|
1147
2127
|
OMX_NOTIFY_FALLBACK: '0',
|
|
1148
2128
|
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
2129
|
+
OMX_RUNS_DIR: runsPath,
|
|
1149
2130
|
TMUX: '',
|
|
1150
2131
|
TMUX_PANE: '',
|
|
1151
2132
|
});
|
|
1152
2133
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1153
2134
|
return;
|
|
1154
2135
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1155
|
-
assert.equal(result.status,
|
|
1156
|
-
assert.match(result.
|
|
1157
|
-
assert.
|
|
1158
|
-
assert.doesNotMatch(
|
|
2136
|
+
assert.equal(result.status, 1, result.error || result.stderr || result.stdout);
|
|
2137
|
+
assert.match(result.stderr, /detached launch safety failure during inert-session/);
|
|
2138
|
+
assert.doesNotMatch(result.stderr, /unsafe-tmux-handle-secret/);
|
|
2139
|
+
assert.doesNotMatch(result.stdout, /fake-codex/);
|
|
2140
|
+
assert.match(tmuxLog, /tmux:-V/);
|
|
2141
|
+
assert.match(tmuxLog, /tmux:d0-runs:missing/);
|
|
2142
|
+
assert.match(tmuxLog, /tmux:new-session/);
|
|
2143
|
+
assert.doesNotMatch(tmuxLog, /tmux:list-sessions|attach-session/);
|
|
1159
2144
|
}
|
|
1160
2145
|
finally {
|
|
1161
|
-
await rm(wd, { recursive: true, force: true });
|
|
2146
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1162
2147
|
}
|
|
1163
2148
|
});
|
|
1164
|
-
it('
|
|
2149
|
+
it('preserves detached ownership when attaching the released tmux session fails', async () => {
|
|
1165
2150
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-fail-'));
|
|
1166
2151
|
try {
|
|
1167
2152
|
const home = join(wd, 'home');
|
|
@@ -1183,7 +2168,7 @@ case "$1" in
|
|
|
1183
2168
|
exit 0
|
|
1184
2169
|
;;
|
|
1185
2170
|
new-session)
|
|
1186
|
-
printf '
|
|
2171
|
+
printf '%%12\n'
|
|
1187
2172
|
exit 0
|
|
1188
2173
|
;;
|
|
1189
2174
|
split-window)
|
|
@@ -1213,6 +2198,7 @@ esac
|
|
|
1213
2198
|
exit 0
|
|
1214
2199
|
`);
|
|
1215
2200
|
await chmod(fakeTmuxPath, 0o755);
|
|
2201
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1216
2202
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1217
2203
|
HOME: home,
|
|
1218
2204
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -1224,17 +2210,18 @@ exit 0
|
|
|
1224
2210
|
});
|
|
1225
2211
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1226
2212
|
return;
|
|
2213
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
1227
2214
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1228
|
-
assert.equal(result.status,
|
|
1229
|
-
assert.match(result.
|
|
2215
|
+
assert.equal(result.status, 1, result.error || result.stderr || result.stdout);
|
|
2216
|
+
assert.match(result.stderr, /detached launch safety failure during post-release-attach/);
|
|
1230
2217
|
assert.match(tmuxLog, /tmux:attach-session -t /);
|
|
1231
|
-
assert.
|
|
2218
|
+
assert.doesNotMatch(tmuxLog, /tmux:kill-session -t /);
|
|
1232
2219
|
}
|
|
1233
2220
|
finally {
|
|
1234
|
-
await rm(wd, { recursive: true, force: true });
|
|
2221
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1235
2222
|
}
|
|
1236
2223
|
});
|
|
1237
|
-
it('
|
|
2224
|
+
it('does not probe or fall back after a released WSL attach returns immediately', async () => {
|
|
1238
2225
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-noop-'));
|
|
1239
2226
|
try {
|
|
1240
2227
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (tmuxLogPath) => `#!/bin/sh
|
|
@@ -1244,7 +2231,7 @@ case "$1" in
|
|
|
1244
2231
|
exit 0
|
|
1245
2232
|
;;
|
|
1246
2233
|
new-session)
|
|
1247
|
-
printf '
|
|
2234
|
+
printf '%%12\n'
|
|
1248
2235
|
exit 0
|
|
1249
2236
|
;;
|
|
1250
2237
|
split-window)
|
|
@@ -1281,15 +2268,12 @@ exit 0
|
|
|
1281
2268
|
return;
|
|
1282
2269
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1283
2270
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1284
|
-
assert.
|
|
1285
|
-
assert.match(result.stderr, /attach-session returned immediately without attaching a client/i);
|
|
1286
|
-
assert.match(result.stderr, /Falling back to direct Codex launch/i);
|
|
2271
|
+
assert.doesNotMatch(result.stderr, /Falling back to direct Codex launch|attach-session returned immediately/);
|
|
1287
2272
|
assert.match(tmuxLog, /tmux:attach-session -t /);
|
|
1288
|
-
assert.
|
|
1289
|
-
assert.match(tmuxLog, /tmux:kill-session -t /);
|
|
2273
|
+
assert.doesNotMatch(tmuxLog, /tmux:display-message -p -t .* #\{session_attached\}|tmux:kill-session -t/);
|
|
1290
2274
|
}
|
|
1291
2275
|
finally {
|
|
1292
|
-
await rm(wd, { recursive: true, force: true });
|
|
2276
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1293
2277
|
}
|
|
1294
2278
|
});
|
|
1295
2279
|
it('preserves the requested cwd through detached tmux launch when an unsupported SHELL value falls back away from rc-driven cwd drift', async () => {
|
|
@@ -1325,11 +2309,7 @@ case "$cmd" in
|
|
|
1325
2309
|
exit 0
|
|
1326
2310
|
;;
|
|
1327
2311
|
new-session)
|
|
1328
|
-
|
|
1329
|
-
if [ -n "\${last:-}" ]; then
|
|
1330
|
-
/bin/sh -c "$last"
|
|
1331
|
-
fi
|
|
1332
|
-
printf 'leader-pane\\n'
|
|
2312
|
+
printf '%%12\n'
|
|
1333
2313
|
exit 0
|
|
1334
2314
|
;;
|
|
1335
2315
|
split-window)
|
|
@@ -1355,6 +2335,7 @@ esac
|
|
|
1355
2335
|
exit 0
|
|
1356
2336
|
`);
|
|
1357
2337
|
await chmod(fakeTmuxPath, 0o755);
|
|
2338
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1358
2339
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1359
2340
|
HOME: home,
|
|
1360
2341
|
SHELL: '/definitely/missing-shell',
|
|
@@ -1367,13 +2348,14 @@ exit 0
|
|
|
1367
2348
|
});
|
|
1368
2349
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1369
2350
|
return;
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
assert.match(
|
|
2351
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
2352
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
2353
|
+
assert.match(tmuxLog, /\/bin\/sh/);
|
|
2354
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
1373
2355
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1374
2356
|
}
|
|
1375
2357
|
finally {
|
|
1376
|
-
await rm(wd, { recursive: true, force: true });
|
|
2358
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1377
2359
|
}
|
|
1378
2360
|
});
|
|
1379
2361
|
it('falls back to /bin/sh for detached tmux launch when SHELL drifts to an unsupported path', async () => {
|
|
@@ -1407,11 +2389,7 @@ case "$cmd" in
|
|
|
1407
2389
|
exit 0
|
|
1408
2390
|
;;
|
|
1409
2391
|
new-session)
|
|
1410
|
-
|
|
1411
|
-
if [ -n "\${last:-}" ]; then
|
|
1412
|
-
/bin/sh -c "$last"
|
|
1413
|
-
fi
|
|
1414
|
-
printf 'leader-pane\\n'
|
|
2392
|
+
printf '%%12\n'
|
|
1415
2393
|
exit 0
|
|
1416
2394
|
;;
|
|
1417
2395
|
split-window)
|
|
@@ -1437,6 +2415,7 @@ esac
|
|
|
1437
2415
|
exit 0
|
|
1438
2416
|
`);
|
|
1439
2417
|
await chmod(fakeTmuxPath, 0o755);
|
|
2418
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1440
2419
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1441
2420
|
HOME: home,
|
|
1442
2421
|
SHELL: '/bin/not-a-real-shell',
|
|
@@ -1450,15 +2429,199 @@ exit 0
|
|
|
1450
2429
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1451
2430
|
return;
|
|
1452
2431
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1453
|
-
const codexLog = normalizeDarwinTmpPath(await readFile(codexLogPath, 'utf-8'));
|
|
1454
2432
|
assert.match(tmuxLog, /\/bin\/sh/);
|
|
1455
2433
|
assert.doesNotMatch(tmuxLog, /not-a-real-shell/);
|
|
1456
|
-
assert.match(
|
|
1457
|
-
assert.match(codexLog, new RegExp(`codex-pwd:${escapeRegExp(normalizeDarwinTmpPath(wd))}`));
|
|
2434
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
1458
2435
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
2436
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
2437
|
+
}
|
|
2438
|
+
finally {
|
|
2439
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
it('keeps finalization authority in the real detached leader process through child exit', async () => {
|
|
2443
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-detached-leader-process-'));
|
|
2444
|
+
const home = join(wd, 'home');
|
|
2445
|
+
const fakeBin = join(wd, 'bin');
|
|
2446
|
+
const fakeChild = join(wd, 'fake-codex.sh');
|
|
2447
|
+
const childReady = join(wd, 'child-ready');
|
|
2448
|
+
const childRelease = join(wd, 'child-release');
|
|
2449
|
+
const childEnv = join(wd, 'child-env');
|
|
2450
|
+
const descendantPidPath = join(wd, 'descendant-pid');
|
|
2451
|
+
const readyPath = join(wd, '.omx', 'runtime', 'detached-release', 'ready');
|
|
2452
|
+
const omxBin = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'cli', 'omx.js');
|
|
2453
|
+
const sessionId = 'omx-detached-leader-process';
|
|
2454
|
+
try {
|
|
2455
|
+
await mkdir(home, { recursive: true });
|
|
2456
|
+
await mkdir(fakeBin, { recursive: true });
|
|
2457
|
+
await writeExecutable(join(fakeBin, 'tmux'), `#!/bin/sh
|
|
2458
|
+
if [ "$1" = "list-panes" ]; then
|
|
2459
|
+
printf '%s\t0\t%s\n' "$TMUX_PANE" "$PPID"
|
|
2460
|
+
fi
|
|
2461
|
+
exit 0
|
|
2462
|
+
`);
|
|
2463
|
+
await writeExecutable(fakeChild, `#!/bin/sh\nprintf '%s' "$OMX_NOTIFY_TEMP_CONTRACT" > ${JSON.stringify(childEnv)}\n(sh -c 'trap "" TERM; while :; do sleep 1; done') &\nprintf '%s' "$!" > ${JSON.stringify(descendantPidPath)}\nprintf ready > ${JSON.stringify(childReady)}\nwhile [ ! -f ${JSON.stringify(childRelease)} ]; do sleep 0.02; done\nexit 0\n`);
|
|
2464
|
+
const payload = Buffer.from(JSON.stringify({
|
|
2465
|
+
cwd: wd,
|
|
2466
|
+
sessionName: 'omx-detached-leader-process',
|
|
2467
|
+
sessionId,
|
|
2468
|
+
codexCmd: fakeChild,
|
|
2469
|
+
readyPath,
|
|
2470
|
+
preLaunchOptions: {
|
|
2471
|
+
notifyTempContract: {
|
|
2472
|
+
active: true,
|
|
2473
|
+
selectors: ['custom:test-provider'],
|
|
2474
|
+
canonicalSelectors: ['custom:test-provider'],
|
|
2475
|
+
warnings: [],
|
|
2476
|
+
source: 'cli',
|
|
2477
|
+
},
|
|
2478
|
+
enableNotifyFallbackAuthority: false,
|
|
2479
|
+
worktreeDirty: false,
|
|
2480
|
+
shouldAttach: true,
|
|
2481
|
+
},
|
|
2482
|
+
})).toString('base64url');
|
|
2483
|
+
const leader = spawn(process.execPath, [omxBin, '__detached-session-leader', payload], {
|
|
2484
|
+
cwd: wd,
|
|
2485
|
+
env: buildRunOmxEnv({
|
|
2486
|
+
HOME: home,
|
|
2487
|
+
TMUX: '/tmp/fake-tmux,1,0',
|
|
2488
|
+
TMUX_PANE: '%3202',
|
|
2489
|
+
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
2490
|
+
OMX_AUTO_UPDATE: '0',
|
|
2491
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
2492
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
2493
|
+
}),
|
|
2494
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
2495
|
+
});
|
|
2496
|
+
await waitForPath(readyPath);
|
|
2497
|
+
const ready = JSON.parse(await readFile(readyPath, 'utf-8'));
|
|
2498
|
+
await writeFile(`${readyPath}.release`, `${JSON.stringify({ version: 1, kind: 'release', nonce: `${ready.nonce}-foreign`, sessionId: ready.sessionId, sessionName: ready.sessionName, leaderPid: ready.leaderPid })}\n`);
|
|
2499
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
2500
|
+
assert.equal(existsSync(childReady), false);
|
|
2501
|
+
await writeFile(`${readyPath}.release`, `${JSON.stringify({ version: 1, kind: 'release', nonce: ready.nonce, sessionId: ready.sessionId, sessionName: ready.sessionName, leaderPid: ready.leaderPid })}\n`);
|
|
2502
|
+
await waitForPath(childReady);
|
|
2503
|
+
assert.match(await readFile(childEnv, 'utf-8'), /custom:test-provider/);
|
|
2504
|
+
await waitForPath(descendantPidPath);
|
|
2505
|
+
const descendantPid = Number.parseInt((await readFile(descendantPidPath, 'utf-8')).trim(), 10);
|
|
2506
|
+
assert.equal(Number.isSafeInteger(descendantPid) && descendantPid > 0, true);
|
|
2507
|
+
assert.equal(existsSync(readyPath), true);
|
|
2508
|
+
const sessionState = JSON.parse(await readFile(join(wd, '.omx', 'state', 'session.json'), 'utf-8'));
|
|
2509
|
+
assert.equal(sessionState.tmux_session_name, 'omx-detached-leader-process');
|
|
2510
|
+
assert.equal(sessionState.tmux_pane_id, '%3202');
|
|
2511
|
+
assert.equal(existsSync(join(wd, '.omx', 'state', 'detached-active-record.json')), true);
|
|
2512
|
+
await writeFile(childRelease, 'release\n');
|
|
2513
|
+
const exitCode = await new Promise((resolveExit, rejectExit) => {
|
|
2514
|
+
leader.once('error', rejectExit);
|
|
2515
|
+
leader.once('exit', resolveExit);
|
|
2516
|
+
});
|
|
2517
|
+
assert.equal(exitCode, 0);
|
|
2518
|
+
assert.throws(() => process.kill(descendantPid, 0), (error) => error.code === 'ESRCH');
|
|
2519
|
+
const terminalReport = JSON.parse(await readFile(readyPath, 'utf-8'));
|
|
2520
|
+
assert.deepEqual(terminalReport, {
|
|
2521
|
+
version: 1,
|
|
2522
|
+
kind: 'terminal',
|
|
2523
|
+
nonce: ready.nonce,
|
|
2524
|
+
sessionId: ready.sessionId,
|
|
2525
|
+
sessionName: ready.sessionName,
|
|
2526
|
+
leaderPid: ready.leaderPid,
|
|
2527
|
+
paneId: '%3202',
|
|
2528
|
+
finalized: true,
|
|
2529
|
+
exitStatus: 0,
|
|
2530
|
+
});
|
|
2531
|
+
assert.equal(existsSync(join(wd, '.omx', 'state', 'session.json')), false);
|
|
2532
|
+
assert.equal(existsSync(join(wd, '.omx', 'state', 'detached-active-record.json')), false);
|
|
2533
|
+
assert.equal(existsSync(readyPath), true);
|
|
2534
|
+
await rm(readyPath, { force: true });
|
|
2535
|
+
assert.match(await readFile(join(wd, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'), new RegExp(sessionId));
|
|
2536
|
+
}
|
|
2537
|
+
finally {
|
|
2538
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
it('returns the attached client to the shell with the child status after a normal detached child exit', async (t) => {
|
|
2542
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
2543
|
+
return;
|
|
2544
|
+
if (!skipUnlessScriptAvailable(t))
|
|
2545
|
+
return;
|
|
2546
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-detached-e2e-'));
|
|
2547
|
+
try {
|
|
2548
|
+
await withTempTmuxSession(async (fixture) => {
|
|
2549
|
+
const home = join(wd, 'home');
|
|
2550
|
+
const bin = join(wd, 'bin');
|
|
2551
|
+
await mkdir(home, { recursive: true });
|
|
2552
|
+
await mkdir(bin, { recursive: true });
|
|
2553
|
+
// The PTY is required for detached attach behavior, but it would also
|
|
2554
|
+
// trigger OMX's unrelated one-time interactive star prompt. Mark it
|
|
2555
|
+
// handled so this harness deterministically exercises only launch exit.
|
|
2556
|
+
await mkdir(join(home, '.omx', 'state'), { recursive: true });
|
|
2557
|
+
await writeFile(join(home, '.omx', 'state', 'star-prompt.json'), '{"prompted_at":"2026-01-01T00:00:00.000Z"}\n');
|
|
2558
|
+
await fixture.createPathShim(bin);
|
|
2559
|
+
const omxBin = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'cli', 'omx.js');
|
|
2560
|
+
const runPtyLaunch = (childExitStatus, remainOnExit) => {
|
|
2561
|
+
writeFileSync(join(bin, 'codex'), `#!/bin/sh\nsleep 1\nexit ${childExitStatus}\n`, { mode: 0o755 });
|
|
2562
|
+
// Simulate a user tmux config that would retain the dead leader pane.
|
|
2563
|
+
fixture.run(['set-option', '-g', 'remain-on-exit', remainOnExit]);
|
|
2564
|
+
const envPrefix = [
|
|
2565
|
+
'unset TMUX TMUX_PANE',
|
|
2566
|
+
'export OMX_TEST_DETACHED_TRACE=1',
|
|
2567
|
+
`export OMX_TEST_DETACHED_TRACE_PATH=${JSON.stringify(join(wd, 'detached-trace.log'))}`,
|
|
2568
|
+
`export HOME=${JSON.stringify(home)}`,
|
|
2569
|
+
`export PATH=${JSON.stringify(bin)}:$PATH`,
|
|
2570
|
+
'export OMX_AUTO_UPDATE=0',
|
|
2571
|
+
'export OMX_NOTIFY_FALLBACK=0',
|
|
2572
|
+
'export OMX_HOOK_DERIVED_SIGNALS=0',
|
|
2573
|
+
'export TERM=xterm-256color',
|
|
2574
|
+
].join('; ');
|
|
2575
|
+
const command = `${envPrefix}; cd ${JSON.stringify(wd)} || exit 125; ${JSON.stringify(process.execPath)} ${JSON.stringify(omxBin)} --tmux ${JSON.stringify('e2e prompt')}; child_status=$?; printf '\n%s:%s\n' 'omx-follow-up' "$child_status"; exit "$child_status"`;
|
|
2576
|
+
if (process.platform === 'darwin') {
|
|
2577
|
+
const result = fixture.runPtyResult(command, { pollLimit: DARWIN_PTY_FINALIZATION_POLL_LIMIT });
|
|
2578
|
+
let diagnostics = '';
|
|
2579
|
+
if (result.error) {
|
|
2580
|
+
const sessions = fixture.runResult(['list-sessions', '-F', '#{session_name}']);
|
|
2581
|
+
for (const session of sessions.stdout.split('\n').filter((name) => name.startsWith('omx-'))) {
|
|
2582
|
+
const panes = fixture.runResult([
|
|
2583
|
+
'list-panes', '-t', session, '-F', '#{pane_id}\t#{pane_dead}\t#{pane_dead_status}\t#{pane_pid}\t#{pane_current_command}\t#{@omx_instance_id}\t#{@omx_hud_owner}',
|
|
2584
|
+
]);
|
|
2585
|
+
diagnostics += `\n[${session}]\n${panes.stdout}`;
|
|
2586
|
+
for (const paneLine of panes.stdout.split('\n').filter(Boolean)) {
|
|
2587
|
+
const [paneId] = paneLine.split('\t', 1);
|
|
2588
|
+
if (!paneId)
|
|
2589
|
+
continue;
|
|
2590
|
+
diagnostics += `\n[${paneLine}]\n${fixture.runResult(['capture-pane', '-p', '-t', paneId, '-S', '-']).stdout}`;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
const tracePath = join(wd, 'detached-trace.log');
|
|
2594
|
+
if (existsSync(tracePath))
|
|
2595
|
+
diagnostics += `\n[detached-trace]\n${readFileSync(tracePath, 'utf-8')}`;
|
|
2596
|
+
}
|
|
2597
|
+
return { status: result.status, output: `${result.stdout}\n${result.stderr}${diagnostics}`, error: result.error };
|
|
2598
|
+
}
|
|
2599
|
+
const ptyCommand = buildPtyScriptCommand(command);
|
|
2600
|
+
const result = spawnSync(ptyCommand.executable, ptyCommand.args, {
|
|
2601
|
+
encoding: 'utf-8',
|
|
2602
|
+
timeout: 120_000,
|
|
2603
|
+
killSignal: 'SIGKILL',
|
|
2604
|
+
env: buildRunOmxEnv({ TMUX: '', TMUX_PANE: '' }),
|
|
2605
|
+
});
|
|
2606
|
+
return { status: result.status, output: `${result.stdout || ''}\n${result.stderr || ''}`, error: result.error?.message ?? '' };
|
|
2607
|
+
};
|
|
2608
|
+
const listOmxSessions = () => fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
2609
|
+
.split('\n')
|
|
2610
|
+
.filter((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
2611
|
+
const zero = runPtyLaunch(0, 'on');
|
|
2612
|
+
assert.equal(zero.error, '', `zero-status PTY capture and cleanup must succeed: ${zero.error}\n${zero.output}`);
|
|
2613
|
+
assert.equal(zero.status, 0, `zero-status attached launch must return to the shell successfully:\n${zero.output}`);
|
|
2614
|
+
assert.match(zero.output, /omx-follow-up:0/, `zero-status launch must leave a usable shell for a follow-up command:\n${zero.output}`);
|
|
2615
|
+
assert.deepEqual(listOmxSessions(), [], 'zero-status exit must destroy the owned detached session despite remain-on-exit=on');
|
|
2616
|
+
const seven = runPtyLaunch(7, 'failed');
|
|
2617
|
+
assert.equal(seven.error, '', `nonzero-status PTY capture and cleanup must succeed: ${seven.error}\n${seven.output}`);
|
|
2618
|
+
assert.equal(seven.status, 7, `nonzero child status must propagate to the invoking shell:\n${seven.output}`);
|
|
2619
|
+
assert.match(seven.output, /omx-follow-up:7/, `nonzero launch must leave a usable shell for a follow-up command:\n${seven.output}`);
|
|
2620
|
+
assert.deepEqual(listOmxSessions(), [], 'nonzero exit must destroy the owned detached session despite remain-on-exit=failed');
|
|
2621
|
+
});
|
|
1459
2622
|
}
|
|
1460
2623
|
finally {
|
|
1461
|
-
await rm(wd, { recursive: true, force: true });
|
|
2624
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1462
2625
|
}
|
|
1463
2626
|
});
|
|
1464
2627
|
});
|