oh-my-codex 0.20.1 → 0.20.3
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 +7 -6
- package/Cargo.toml +1 -1
- package/README.md +36 -1
- package/crates/omx-runtime/Cargo.toml +1 -0
- package/crates/omx-runtime/src/main.rs +25 -2
- package/crates/omx-runtime/tests/execution.rs +219 -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__/hermes.test.js.map +1 -1
- package/dist/adapt/contracts.d.ts.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.d.ts +2 -0
- package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
- package/dist/auth/__tests__/hotswap.test.js +183 -0
- package/dist/auth/__tests__/hotswap.test.js.map +1 -0
- package/dist/auth/hotswap.d.ts +6 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +102 -19
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/paths.js.map +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +5 -5
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +1 -1
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/lockfile.js.map +1 -1
- package/dist/catalog/reader.js.map +1 -1
- package/dist/catalog/skill-mirror.js.map +1 -1
- package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +60 -6
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +752 -15
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +136 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +473 -14
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +336 -4
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +87 -12
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +206 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +8 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +72 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +55 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.js +17 -11
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
- 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__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
- package/dist/cli/__tests__/setup-install-mode.test.js +2579 -294
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-preferences.test.js +79 -0
- package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
- package/dist/cli/__tests__/setup-refresh.test.js +33 -2
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +46 -3
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +37 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +105 -7
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +61 -1
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +1817 -24
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +58 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/url.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/adapt.js.map +1 -1
- package/dist/cli/agents-init.js.map +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/ask.d.ts +1 -1
- package/dist/cli/ask.d.ts.map +1 -1
- package/dist/cli/ask.js.map +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch-intake.js.map +1 -1
- package/dist/cli/capabilities.js.map +1 -1
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +21 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +607 -130
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +14 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +245 -57
- 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/mcp-serve.js.map +1 -1
- package/dist/cli/mission.js.map +1 -1
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +29 -0
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
- package/dist/cli/native-hook-claim-journal.js +252 -0
- package/dist/cli/native-hook-claim-journal.js.map +1 -0
- package/dist/cli/omx.js.map +1 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +10 -0
- package/dist/cli/ralplan.d.ts.map +1 -0
- package/dist/cli/ralplan.js +110 -0
- package/dist/cli/ralplan.js.map +1 -0
- 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 +11 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +32 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +58 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1450 -398
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +2 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +5 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +34 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +17 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +986 -95
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +8 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/url.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +5 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +1287 -11
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +1179 -32
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +6 -1
- 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/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +124 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2142 -265
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +23 -34
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +1480 -246
- package/dist/config/generator.js.map +1 -1
- package/dist/config/mcp-registry.js.map +1 -1
- package/dist/config/models.d.ts +14 -4
- 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/config/omx-first-party-mcp.js.map +1 -1
- package/dist/config/team-mode.d.ts.map +1 -1
- package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
- package/dist/document-refresh/enforcer.js.map +1 -1
- package/dist/exec/followup.js.map +1 -1
- package/dist/goal-workflows/artifacts.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +1376 -6
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +252 -543
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -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 +765 -10
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -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 +142 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +163 -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-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-visual-verdict.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +878 -11
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +5 -5
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/code-simplifier/index.js.map +1 -1
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/events.js.map +1 -1
- package/dist/hooks/extensibility/loader.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
- package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +33 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +2228 -119
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts +12 -0
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +41 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-session-provenance.d.ts +87 -0
- package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
- package/dist/hooks/prompt-session-provenance.js +180 -0
- package/dist/hooks/prompt-session-provenance.js.map +1 -0
- package/dist/hooks/session.d.ts +163 -34
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1395 -317
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +153 -1
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +42 -5
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +184 -3
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.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__/server-lifecycle.test.js +15 -2
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +98 -1
- package/dist/mcp/__tests__/state-paths.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/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +58 -58
- 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.js.map +1 -1
- package/dist/mcp/hermes-server.d.ts +70 -70
- package/dist/mcp/hermes-server.d.ts.map +1 -1
- package/dist/mcp/lifecycle-telemetry.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +34 -34
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +14 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +96 -20
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +47 -47
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.d.ts +4 -4
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +34 -23
- 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-session-scope.test.js +53 -1
- 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.map +1 -1
- package/dist/modes/base.js +5 -5
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/index.d.ts +4 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +3 -9
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/lifecycle-dedupe.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/session-registry.js.map +1 -1
- package/dist/notifications/session-status.js.map +1 -1
- 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/notifications/tmux-detector.js.map +1 -1
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +16 -13
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/pipeline/stages/ultraqa.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/planning/artifact-names.js.map +1 -1
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/client.d.ts.map +1 -1
- package/dist/question/client.js.map +1 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/events.js.map +1 -1
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js.map +1 -1
- package/dist/question/ui.js.map +1 -1
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +527 -8
- 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 +46 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +116 -14
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +3 -3
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +173 -70
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +58 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +3 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +12 -9
- 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/process-tree.js.map +1 -1
- package/dist/runtime/run-loop.js.map +1 -1
- package/dist/runtime/run-outcome.d.ts +3 -3
- package/dist/runtime/run-outcome.d.ts.map +1 -1
- package/dist/runtime/run-outcome.js.map +1 -1
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +6657 -995
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
- package/dist/scripts/__tests__/notify-state-io.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.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +78 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +55 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +1100 -4
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
- package/dist/scripts/build-api.js.map +1 -1
- package/dist/scripts/build-explore-harness.js.map +1 -1
- package/dist/scripts/build-sparkshell.js.map +1 -1
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-execution-surface.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +4 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +11577 -1184
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/generate-release-body.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
- package/dist/scripts/hook-payload-guard.js.map +1 -1
- package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
- package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
- package/dist/scripts/notify-fallback-delivery.js +419 -0
- package/dist/scripts/notify-fallback-delivery.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +218 -138
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +112 -15
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +122 -12
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts +17 -0
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +73 -11
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +10 -1
- 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 +2 -2
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +16 -8
- 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-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +5 -2
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +37 -28
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +53 -18
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +15 -13
- package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts +24 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +267 -90
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +927 -5
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +4317 -13
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/test-reply-listener-live.js.map +1 -1
- package/dist/scripts/test-sparkshell.js.map +1 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/session-history/friction.js.map +1 -1
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
- package/dist/sidecar/collector.js.map +1 -1
- package/dist/sidecar/index.js.map +1 -1
- package/dist/sidecar/render.js.map +1 -1
- package/dist/sidecar/tmux.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +54 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +66 -15
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +19 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +61 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +5 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +59 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +49 -55
- package/dist/state/operations.js.map +1 -1
- package/dist/state/paths.d.ts +1 -1
- package/dist/state/paths.d.ts.map +1 -1
- package/dist/state/paths.js +1 -1
- package/dist/state/paths.js.map +1 -1
- package/dist/state/skill-active.d.ts +13 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +43 -4
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/terminal-normalization.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +20 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +52 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
- package/dist/subagents/__tests__/tracker.test.js +1550 -10
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/role-routing-marker.d.ts +10 -0
- package/dist/subagents/role-routing-marker.d.ts.map +1 -0
- package/dist/subagents/role-routing-marker.js +136 -0
- package/dist/subagents/role-routing-marker.js.map +1 -0
- package/dist/subagents/tracker.d.ts +91 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +806 -62
- 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__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
- 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 +93 -27
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +341 -4
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/role-router.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 +3624 -355
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2516 -118
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- 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 +3664 -969
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
- 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 +2 -2
- 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/commit-hygiene.js.map +1 -1
- package/dist/team/contracts.d.ts +5 -5
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/delivery-log.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/followup-planner.js.map +1 -1
- package/dist/team/goal-workflow.js.map +1 -1
- 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 +18 -2
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +314 -35
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/pane-status.js.map +1 -1
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/reminder-intents.d.ts +1 -1
- package/dist/team/reminder-intents.d.ts.map +1 -1
- package/dist/team/reminder-intents.js.map +1 -1
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1326 -231
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +22 -8
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1463 -376
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch-lock.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/events.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/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-root.js.map +1 -1
- package/dist/team/state.d.ts +67 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +636 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts.map +1 -1
- package/dist/team/team-identity.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 +125 -23
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1340 -333
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.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.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +4 -1
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/index.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 +57 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/file-durability.d.ts +15 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +37 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/repo-deps.js.map +1 -1
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +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/verifier.js.map +1 -1
- package/dist/visual/constants.d.ts +1 -1
- package/dist/visual/constants.d.ts.map +1 -1
- package/dist/vscode/__tests__/index.test.js.map +1 -1
- package/dist/vscode/index.js.map +1 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
- package/dist/wiki/__tests__/lint.test.js.map +1 -1
- package/dist/wiki/__tests__/query.test.js.map +1 -1
- package/dist/wiki/__tests__/storage.test.js.map +1 -1
- package/dist/wiki/__tests__/test-helpers.d.ts.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/dist/wiki/query.js.map +1 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -2
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +6 -2
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/skills/omx-setup/SKILL.md +10 -5
- package/skills/ralph/SKILL.md +6 -2
- package/skills/ralplan/SKILL.md +6 -2
- package/skills/team/SKILL.md +13 -9
- package/src/scripts/__tests__/codex-native-hook.test.ts +20048 -10827
- package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
- package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +78 -0
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +60 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +1309 -4
- package/src/scripts/codex-native-hook.ts +11429 -1163
- package/src/scripts/notify-fallback-delivery.ts +416 -0
- package/src/scripts/notify-fallback-watcher.ts +216 -155
- package/src/scripts/notify-hook/auto-nudge.ts +113 -14
- package/src/scripts/notify-hook/managed-tmux.ts +149 -12
- package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
- package/src/scripts/notify-hook/state-io.ts +100 -13
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
- package/src/scripts/notify-hook/team-worker.ts +39 -26
- package/src/scripts/notify-hook/tmux-injection.ts +66 -23
- package/src/scripts/notify-hook/visual-verdict.ts +22 -12
- package/src/scripts/notify-hook.ts +336 -90
- package/src/scripts/smoke-packed-install.ts +5154 -149
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { after, before, describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile, stat, } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { parse as parseToml } from "@iarna/toml";
|
|
8
|
-
import { setup } from "../setup.js";
|
|
8
|
+
import { decideWindowsNativeHookShimReference, setNativeHookTransactionFailureInjectorForTest, setSetupLatePhaseFailureInjectorForTest, setNativeHookTransactionPlatformForTest, setNativeHookTransactionRegularFileSyncForTest, setNativeHookTransactionTemporaryPathForTest, setup, } from "../setup.js";
|
|
9
|
+
import { resolveSetupRefreshArgs } from "../update.js";
|
|
9
10
|
import { uninstall } from "../uninstall.js";
|
|
10
11
|
import { OMX_DEVELOPER_INSTRUCTIONS, OMX_PLUGIN_DEVELOPER_INSTRUCTIONS, } from "../../config/generator.js";
|
|
11
12
|
import { materializePackagedOmxPluginCache, resolvePackagedOmxMarketplace, } from "../plugin-marketplace.js";
|
|
13
|
+
import { buildManagedCodexHookTrustState, buildManagedCodexHooksConfig, buildManagedCodexNativeHookWindowsShimContent, buildManagedCodexNativeHookWindowsShimPath, } from "../../config/codex-hooks.js";
|
|
12
14
|
const packageRoot = process.cwd();
|
|
13
15
|
let previousPathForFakeCodex;
|
|
14
16
|
let fakeCodexBinDir = null;
|
|
@@ -96,6 +98,32 @@ async function withIsolatedUserHome(wd, fn) {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
async function withDriveQualifiedWindowsCodexHome(wd, fn) {
|
|
102
|
+
const previousCwd = process.cwd();
|
|
103
|
+
const previousHome = process.env.HOME;
|
|
104
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
105
|
+
const codexHomeDir = "C:\\Users\\omx\\.codex";
|
|
106
|
+
process.chdir(wd);
|
|
107
|
+
process.env.HOME = wd;
|
|
108
|
+
process.env.CODEX_HOME = codexHomeDir;
|
|
109
|
+
try {
|
|
110
|
+
await mkdir(codexHomeDir, { recursive: true });
|
|
111
|
+
return await fn(codexHomeDir);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
process.chdir(previousCwd);
|
|
115
|
+
if (typeof previousHome === "string")
|
|
116
|
+
process.env.HOME = previousHome;
|
|
117
|
+
else
|
|
118
|
+
delete process.env.HOME;
|
|
119
|
+
if (typeof previousCodexHome === "string") {
|
|
120
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
delete process.env.CODEX_HOME;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
99
127
|
describe("notify setup scope", () => {
|
|
100
128
|
it("does not write unsupported project-scope notify", async () => {
|
|
101
129
|
const wd = await mkdtemp(join(tmpdir(), "omx-project-no-notify-"));
|
|
@@ -1357,6 +1385,12 @@ describe("omx setup install mode behavior", () => {
|
|
|
1357
1385
|
assert.match(config, /Registered Codex plugin marketplace surfaces supply OMX workflows and plugin-scoped companion resources/);
|
|
1358
1386
|
assert.match(config, /User-installed skills may still live under ~\/.codex\/skills/);
|
|
1359
1387
|
assert.match(config, /native agent roles are installed as setup-owned Codex agent TOML files in plugin mode so agent_type routing works/i);
|
|
1388
|
+
assert.match(config, /When the native surface exposes `agent_type` role routing, set `agent_type` to an installed role and never omit it for OMX work/i);
|
|
1389
|
+
assert.match(config, /role_routing_unavailable/i);
|
|
1390
|
+
assert.match(config, /do not fabricate `agent_type`/i);
|
|
1391
|
+
assert.match(config, /omx ralplan preflight --json/i);
|
|
1392
|
+
assert.match(config, /unsupported_documented_leader_proof/i);
|
|
1393
|
+
assert.match(config, /never fake the role via a prompt label/i);
|
|
1360
1394
|
assert.doesNotMatch(config, /Native subagents live in \.codex\/agents/);
|
|
1361
1395
|
assert.doesNotMatch(config, /Treat installed prompts as narrower execution surfaces/);
|
|
1362
1396
|
assert.match(config, /^plugin_hooks = true$/m);
|
|
@@ -1463,7 +1497,7 @@ describe("omx setup install mode behavior", () => {
|
|
|
1463
1497
|
await rm(wd, { recursive: true, force: true });
|
|
1464
1498
|
}
|
|
1465
1499
|
});
|
|
1466
|
-
it("
|
|
1500
|
+
it("migrates historical unwrapped developer_instructions after prompting", async () => {
|
|
1467
1501
|
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
1468
1502
|
try {
|
|
1469
1503
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
@@ -1480,10 +1514,12 @@ describe("omx setup install mode behavior", () => {
|
|
|
1480
1514
|
return "refresh";
|
|
1481
1515
|
},
|
|
1482
1516
|
});
|
|
1483
|
-
assert.equal(promptCount,
|
|
1517
|
+
assert.equal(promptCount, 1);
|
|
1484
1518
|
const config = await readFile(configPath, "utf-8");
|
|
1485
|
-
assert.match(config,
|
|
1486
|
-
assert.
|
|
1519
|
+
assert.match(config, /<omx version=\\"1\\">You have oh-my-codex installed through Codex plugin mode/);
|
|
1520
|
+
assert.match(config, /When the native surface exposes `agent_type` role routing, set `agent_type` to an installed role and never omit it for OMX work/i);
|
|
1521
|
+
assert.match(config, /role_routing_unavailable/i);
|
|
1522
|
+
assert.match(config, /omx ralplan preflight --json/i);
|
|
1487
1523
|
assert.equal((config.match(/^developer_instructions\s*=/gm) ?? []).length, 1);
|
|
1488
1524
|
});
|
|
1489
1525
|
});
|
|
@@ -1734,463 +1770,2712 @@ describe("omx setup install mode behavior", () => {
|
|
|
1734
1770
|
await rm(wd, { recursive: true, force: true });
|
|
1735
1771
|
}
|
|
1736
1772
|
});
|
|
1737
|
-
it("
|
|
1738
|
-
const
|
|
1773
|
+
it("fails before plugin transition writes when marker-wrapped trust state has foreign siblings", async () => {
|
|
1774
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1775
|
+
const representations = [
|
|
1776
|
+
{
|
|
1777
|
+
name: "inline",
|
|
1778
|
+
render: (key, hash) => [
|
|
1779
|
+
`hooks = { state = { ${JSON.stringify(key)} = { trusted_hash = ${JSON.stringify(hash)}, foreign = true } } }`,
|
|
1780
|
+
],
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
name: "dotted",
|
|
1784
|
+
render: (key, hash) => [
|
|
1785
|
+
`hooks.state.${JSON.stringify(key)}.trusted_hash = ${JSON.stringify(hash)}`,
|
|
1786
|
+
`hooks.state.${JSON.stringify(key)}.foreign = true`,
|
|
1787
|
+
],
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
name: "table",
|
|
1791
|
+
render: (key, hash) => [
|
|
1792
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
1793
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
1794
|
+
"foreign = true",
|
|
1795
|
+
],
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
name: "separate-foreign-table",
|
|
1799
|
+
render: (key, hash) => [
|
|
1800
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
1801
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
1802
|
+
"",
|
|
1803
|
+
'[hooks.state."foreign-key"]',
|
|
1804
|
+
'trusted_hash = "sha256:foreign"',
|
|
1805
|
+
],
|
|
1806
|
+
},
|
|
1807
|
+
];
|
|
1739
1808
|
try {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1809
|
+
for (const representation of representations) {
|
|
1810
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-plugin-trust-${representation.name}-`));
|
|
1811
|
+
try {
|
|
1812
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1813
|
+
await withTempCwd(wd, async () => {
|
|
1814
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
1815
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
1816
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
1817
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
1818
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot, {
|
|
1819
|
+
platform: "win32",
|
|
1820
|
+
codexHomeDir,
|
|
1821
|
+
}), null, 2)}\n`;
|
|
1822
|
+
const [key, trust] = Object.entries(buildManagedCodexHookTrustState(hooksPath, packageRoot, {
|
|
1823
|
+
platform: "win32",
|
|
1824
|
+
codexHomeDir,
|
|
1825
|
+
hooksContent: hooks,
|
|
1826
|
+
}))[0] ?? [];
|
|
1827
|
+
assert.ok(key);
|
|
1828
|
+
assert.ok(trust);
|
|
1829
|
+
const config = [
|
|
1830
|
+
'model = "foreign"',
|
|
1831
|
+
"",
|
|
1832
|
+
"# ============================================================",
|
|
1833
|
+
"# oh-my-codex (OMX) Configuration",
|
|
1834
|
+
"# Managed by omx setup - manual edits preserved on next setup",
|
|
1835
|
+
"# ============================================================",
|
|
1836
|
+
"",
|
|
1837
|
+
...representation.render(key, trust.trusted_hash),
|
|
1838
|
+
"",
|
|
1839
|
+
"# ============================================================",
|
|
1840
|
+
"# End oh-my-codex",
|
|
1841
|
+
"",
|
|
1842
|
+
].join("\n");
|
|
1843
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
1844
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot), "utf-8");
|
|
1845
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1846
|
+
await writeFile(configPath, config);
|
|
1847
|
+
await writeFile(hooksPath, hooks);
|
|
1848
|
+
await writeFile(shimPath, shim);
|
|
1849
|
+
await writeFile(metadataPath, metadata);
|
|
1850
|
+
await assert.rejects(setup({
|
|
1851
|
+
scope: "user",
|
|
1852
|
+
installMode: "plugin",
|
|
1853
|
+
pluginAgentsMdPrompt: async () => false,
|
|
1854
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
1855
|
+
}), (error) => typeof error === "object" &&
|
|
1856
|
+
error !== null &&
|
|
1857
|
+
"code" in error &&
|
|
1858
|
+
error.code === "managed_trust_key_conflict");
|
|
1859
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, "utf-8"));
|
|
1860
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(hooks, "utf-8"));
|
|
1861
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
1862
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
1863
|
+
assert.deepEqual((await readdir(codexHomeDir)).filter((entry) => entry.includes(".omx-")), []);
|
|
1864
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes(".omx-")), []);
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
finally {
|
|
1869
|
+
await rm(wd, { recursive: true, force: true });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1753
1872
|
}
|
|
1754
1873
|
finally {
|
|
1755
|
-
|
|
1874
|
+
resetPlatform();
|
|
1756
1875
|
}
|
|
1757
1876
|
});
|
|
1758
|
-
it("
|
|
1759
|
-
const
|
|
1877
|
+
it("fails before plugin transition writes when marker trust has absent or non-managed hooks expectations", async () => {
|
|
1878
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1879
|
+
const fixtures = [
|
|
1880
|
+
{ name: "absent", hooks: null },
|
|
1881
|
+
{
|
|
1882
|
+
name: "non-managed",
|
|
1883
|
+
hooks: `${JSON.stringify({
|
|
1884
|
+
hooks: {
|
|
1885
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: "echo foreign" }] }],
|
|
1886
|
+
},
|
|
1887
|
+
}, null, 2)}\n`,
|
|
1888
|
+
},
|
|
1889
|
+
];
|
|
1890
|
+
try {
|
|
1891
|
+
for (const fixture of fixtures) {
|
|
1892
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-plugin-marker-${fixture.name}-`));
|
|
1893
|
+
try {
|
|
1894
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1895
|
+
await withTempCwd(wd, async () => {
|
|
1896
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
1897
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
1898
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
1899
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
1900
|
+
const config = [
|
|
1901
|
+
'model = "foreign"',
|
|
1902
|
+
"",
|
|
1903
|
+
"# ============================================================",
|
|
1904
|
+
"# oh-my-codex (OMX) Configuration",
|
|
1905
|
+
"# Managed by omx setup - manual edits preserved on next setup",
|
|
1906
|
+
"# ============================================================",
|
|
1907
|
+
"",
|
|
1908
|
+
'[hooks.state."foreign-key"]',
|
|
1909
|
+
'trusted_hash = "sha256:foreign"',
|
|
1910
|
+
"",
|
|
1911
|
+
"# ============================================================",
|
|
1912
|
+
"# End oh-my-codex",
|
|
1913
|
+
"",
|
|
1914
|
+
].join("\n");
|
|
1915
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot), "utf-8");
|
|
1916
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
1917
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1918
|
+
await writeFile(configPath, config);
|
|
1919
|
+
if (fixture.hooks !== null)
|
|
1920
|
+
await writeFile(hooksPath, fixture.hooks);
|
|
1921
|
+
await writeFile(shimPath, shim);
|
|
1922
|
+
await writeFile(metadataPath, metadata);
|
|
1923
|
+
await assert.rejects(setup({
|
|
1924
|
+
scope: "user",
|
|
1925
|
+
installMode: "plugin",
|
|
1926
|
+
pluginAgentsMdPrompt: async () => false,
|
|
1927
|
+
skipNativeAgentRefresh: true,
|
|
1928
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
1929
|
+
}), (error) => typeof error === "object" &&
|
|
1930
|
+
error !== null &&
|
|
1931
|
+
"code" in error &&
|
|
1932
|
+
error.code === "managed_trust_key_conflict");
|
|
1933
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, "utf-8"));
|
|
1934
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
1935
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
1936
|
+
if (fixture.hooks === null) {
|
|
1937
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1938
|
+
}
|
|
1939
|
+
else {
|
|
1940
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(fixture.hooks, "utf-8"));
|
|
1941
|
+
}
|
|
1942
|
+
assert.deepEqual((await readdir(codexHomeDir)).filter((entry) => entry.includes(".omx-")), []);
|
|
1943
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes(".omx-")), []);
|
|
1944
|
+
});
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
finally {
|
|
1948
|
+
await rm(wd, { recursive: true, force: true });
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
finally {
|
|
1953
|
+
resetPlatform();
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
it("treats normalized managed Windows shim identity as exact and every distinct target as ambiguous", () => {
|
|
1957
|
+
const shimBasename = "omx-native-hook-windows-shim.ps1";
|
|
1958
|
+
const targetShimPath = `C:\\Users\\alice\\.codex\\hooks\\${shimBasename}`;
|
|
1959
|
+
const futureCommand = (shimPath) => `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimPath}'`;
|
|
1960
|
+
const futureHooks = (shimPath) => JSON.stringify({
|
|
1961
|
+
hooks: {
|
|
1962
|
+
FutureEvent: [{ hooks: [{ type: "command", command: futureCommand(shimPath) }] }],
|
|
1963
|
+
},
|
|
1964
|
+
});
|
|
1965
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(`c:/users/alice/.codex/hooks/../hooks/${shimBasename.toUpperCase()}`), targetShimPath), "referenced");
|
|
1966
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(`D:\\Users\\alice\\.codex\\hooks\\${shimBasename}`), targetShimPath), "ambiguous");
|
|
1967
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks("D:\\Users\\alice\\.codex\\hooks\\unrelated.ps1"), targetShimPath), "ambiguous");
|
|
1968
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1969
|
+
hooks: {
|
|
1970
|
+
FutureEvent: [{ hooks: [{ type: "command", command: "& $env:OMX_SHIM" }] }],
|
|
1971
|
+
},
|
|
1972
|
+
}), targetShimPath), "ambiguous");
|
|
1973
|
+
for (const command of [
|
|
1974
|
+
"& %OMX_SHIM%",
|
|
1975
|
+
"& !OMX_SHIM!",
|
|
1976
|
+
"& $(Get-Item env:OMX_SHIM)",
|
|
1977
|
+
]) {
|
|
1978
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1979
|
+
hooks: {
|
|
1980
|
+
FutureEvent: [{ hooks: [{ type: "command", command }] }],
|
|
1981
|
+
},
|
|
1982
|
+
}), targetShimPath), "ambiguous", command);
|
|
1983
|
+
}
|
|
1984
|
+
for (const command of [
|
|
1985
|
+
`Write-Output "don't"; & $env:HOOK_SCRIPT; Write-Output "user's"`,
|
|
1986
|
+
"Invoke-Expression '& $env:HOOK_SCRIPT'",
|
|
1987
|
+
"cmd /c 'call %HOOK_SCRIPT%'",
|
|
1988
|
+
`& ('C:\\Users\\alice\\.codex\\hooks\\omx-native-hook-windows-' + 'shim.ps1')`,
|
|
1989
|
+
"& (Get-Content C:\\shim-path.txt)",
|
|
1990
|
+
]) {
|
|
1991
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1992
|
+
hooks: {
|
|
1993
|
+
FutureEvent: [{ hooks: [{ type: "command", command }] }],
|
|
1994
|
+
},
|
|
1995
|
+
}), targetShimPath), "ambiguous", command);
|
|
1996
|
+
}
|
|
1997
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1998
|
+
hooks: {
|
|
1999
|
+
FutureEvent: [{
|
|
2000
|
+
hooks: [{
|
|
2001
|
+
type: "command",
|
|
2002
|
+
command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
|
|
2003
|
+
}],
|
|
2004
|
+
}],
|
|
2005
|
+
},
|
|
2006
|
+
}), targetShimPath), "ambiguous");
|
|
2007
|
+
for (const ambiguousPath of [
|
|
2008
|
+
`\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2009
|
+
`C:Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2010
|
+
`C:\\\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2011
|
+
String.raw `\Users\ALICE~1\.codex\hooks\OMX-NA~1.PS1`,
|
|
2012
|
+
String.raw `C:Users\ALICE~1\.codex\hooks\OMX-NA~1.PS1`,
|
|
2013
|
+
`\\\\?\\C:\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2014
|
+
`C:\\$env:USERPROFILE\\hooks\\${shimBasename}`,
|
|
2015
|
+
`.\\hooks\\${shimBasename}`,
|
|
2016
|
+
`D:\\aliases\\${shimBasename}. `,
|
|
2017
|
+
`C:\\Users\\ALICE~1\\.codex\\hooks\\OMX-NA~1.PS1`,
|
|
2018
|
+
]) {
|
|
2019
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(ambiguousPath), targetShimPath), "ambiguous", ambiguousPath);
|
|
2020
|
+
}
|
|
2021
|
+
const uncTargetShimPath = `\\\\server\\share\\users\\alice\\.codex\\hooks\\${shimBasename}`;
|
|
2022
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(`//SERVER/share/users/alice/.codex/hooks/../hooks/${shimBasename.toUpperCase()}`), uncTargetShimPath), "referenced");
|
|
2023
|
+
});
|
|
2024
|
+
it("rolls back when an environment-indirected preserved Windows shim drifts during hooks or config writes", async () => {
|
|
2025
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2026
|
+
try {
|
|
2027
|
+
for (const mutationKind of ["hooks", "config"]) {
|
|
2028
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-plugin-preserved-shim-${mutationKind}-`));
|
|
2029
|
+
let resetFailureInjector;
|
|
2030
|
+
try {
|
|
2031
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2032
|
+
await withTempCwd(wd, async () => {
|
|
2033
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2034
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2035
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2036
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2037
|
+
platform: "win32",
|
|
2038
|
+
codexHomeDir,
|
|
2039
|
+
});
|
|
2040
|
+
const hooksBefore = Buffer.from(`${JSON.stringify({
|
|
2041
|
+
...managed,
|
|
2042
|
+
hooks: {
|
|
2043
|
+
...managed.hooks,
|
|
2044
|
+
FutureEvent: [{
|
|
2045
|
+
hooks: [{ type: "command", command: "& $env:OMX_SHIM" }],
|
|
2046
|
+
}],
|
|
2047
|
+
},
|
|
2048
|
+
}, null, 2)}\n`, "utf-8");
|
|
2049
|
+
const foreignShim = Buffer.from(`# concurrent ${mutationKind} shim replacement\n`, "utf-8");
|
|
2050
|
+
await writeFile(hooksPath, hooksBefore);
|
|
2051
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2052
|
+
let injected = false;
|
|
2053
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
2054
|
+
if (stage !== "before_temp_write" ||
|
|
2055
|
+
target.kind !== mutationKind ||
|
|
2056
|
+
injected) {
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
injected = true;
|
|
2060
|
+
writeFileSync(shimPath, foreignShim);
|
|
2061
|
+
});
|
|
2062
|
+
await assert.rejects(setup({
|
|
2063
|
+
scope: "user",
|
|
2064
|
+
installMode: "plugin",
|
|
2065
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2066
|
+
skipNativeAgentRefresh: true,
|
|
2067
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2068
|
+
}), /precondition changed/);
|
|
2069
|
+
assert.equal(injected, true, mutationKind);
|
|
2070
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
2071
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2072
|
+
assert.equal(existsSync(configPath), false);
|
|
2073
|
+
});
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
finally {
|
|
2077
|
+
resetFailureInjector?.();
|
|
2078
|
+
await rm(wd, { recursive: true, force: true });
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
finally {
|
|
2083
|
+
resetPlatform();
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
it("rolls back when a preserved Windows shim drifts after staged cleanup finalization", async () => {
|
|
2087
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-preserved-shim-finalization-"));
|
|
2088
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2089
|
+
let resetFailureInjector;
|
|
1760
2090
|
try {
|
|
1761
2091
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1762
2092
|
await withTempCwd(wd, async () => {
|
|
1763
2093
|
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
1764
2094
|
const configPath = join(codexHomeDir, "config.toml");
|
|
1765
|
-
|
|
2095
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2096
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2097
|
+
platform: "win32",
|
|
2098
|
+
codexHomeDir,
|
|
2099
|
+
});
|
|
2100
|
+
const hooksBefore = Buffer.from(`${JSON.stringify({
|
|
2101
|
+
...managed,
|
|
1766
2102
|
hooks: {
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
hooks: [
|
|
1770
|
-
|
|
1771
|
-
type: "command",
|
|
1772
|
-
command: "/usr/bin/python3 /tmp/user-hook.py",
|
|
1773
|
-
timeout: 5,
|
|
1774
|
-
},
|
|
1775
|
-
],
|
|
1776
|
-
},
|
|
1777
|
-
],
|
|
2103
|
+
...managed.hooks,
|
|
2104
|
+
FutureEvent: [{
|
|
2105
|
+
hooks: [{ type: "command", command: "& $env:OMX_SHIM" }],
|
|
2106
|
+
}],
|
|
1778
2107
|
},
|
|
1779
|
-
}, null, 2)
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
"
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
assert.
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
assert.
|
|
2108
|
+
}, null, 2)}\n`, "utf-8");
|
|
2109
|
+
const foreignShim = Buffer.from("# concurrent finalization shim replacement\n", "utf-8");
|
|
2110
|
+
await writeFile(hooksPath, hooksBefore);
|
|
2111
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2112
|
+
let injected = false;
|
|
2113
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
2114
|
+
if (stage !== "after_staged_cleanup" || injected)
|
|
2115
|
+
return;
|
|
2116
|
+
injected = true;
|
|
2117
|
+
writeFileSync(shimPath, foreignShim);
|
|
2118
|
+
});
|
|
2119
|
+
await assert.rejects(setup({
|
|
2120
|
+
scope: "user",
|
|
2121
|
+
installMode: "plugin",
|
|
2122
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2123
|
+
skipNativeAgentRefresh: true,
|
|
2124
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2125
|
+
}), /precondition changed/);
|
|
2126
|
+
assert.equal(injected, true);
|
|
2127
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
2128
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2129
|
+
assert.equal(existsSync(configPath), false);
|
|
1798
2130
|
});
|
|
1799
2131
|
});
|
|
1800
2132
|
}
|
|
1801
2133
|
finally {
|
|
2134
|
+
resetFailureInjector?.();
|
|
2135
|
+
resetPlatform();
|
|
1802
2136
|
await rm(wd, { recursive: true, force: true });
|
|
1803
2137
|
}
|
|
1804
2138
|
});
|
|
1805
|
-
it("
|
|
1806
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-
|
|
2139
|
+
it("preserves a drive-qualified shim for a drive-less future reference and aborts stale plugin transitions", async () => {
|
|
2140
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-drive-qualified-future-shim-"));
|
|
2141
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2142
|
+
let resetFailureInjector;
|
|
1807
2143
|
try {
|
|
1808
|
-
await
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
2144
|
+
await withDriveQualifiedWindowsCodexHome(wd, async (codexHomeDir) => {
|
|
2145
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2146
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2147
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2148
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2149
|
+
platform: "win32",
|
|
2150
|
+
codexHomeDir,
|
|
2151
|
+
});
|
|
2152
|
+
const driveLessFutureShimPath = "\\Users\\omx\\.codex\\hooks\\omx-native-hook-windows-shim.ps1";
|
|
2153
|
+
const futureHooks = `${JSON.stringify({
|
|
2154
|
+
...managed,
|
|
2155
|
+
hooks: {
|
|
2156
|
+
...managed.hooks,
|
|
2157
|
+
FutureEvent: [{
|
|
2158
|
+
hooks: [{
|
|
2159
|
+
type: "command",
|
|
2160
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${driveLessFutureShimPath}'`,
|
|
2161
|
+
}],
|
|
2162
|
+
}],
|
|
2163
|
+
},
|
|
2164
|
+
}, null, 2)}\n`;
|
|
2165
|
+
const pluginSetup = () => setup({
|
|
2166
|
+
scope: "user",
|
|
1813
2167
|
installMode: "plugin",
|
|
1814
|
-
|
|
2168
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2169
|
+
skipNativeAgentRefresh: true,
|
|
2170
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
1815
2171
|
});
|
|
1816
|
-
await
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
assert.equal(existsSync(
|
|
1820
|
-
assert.
|
|
2172
|
+
await writeFile(hooksPath, futureHooks);
|
|
2173
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2174
|
+
await pluginSetup();
|
|
2175
|
+
assert.equal(existsSync(shimPath), true);
|
|
2176
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
2177
|
+
await writeFile(hooksPath, futureHooks);
|
|
2178
|
+
const hooksBefore = await readFile(hooksPath);
|
|
2179
|
+
const configBefore = await readFile(configPath);
|
|
2180
|
+
const foreignShim = Buffer.from("# concurrent foreign shim\n", "utf-8");
|
|
2181
|
+
let injected = false;
|
|
2182
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
2183
|
+
if (stage !== "before_precondition" || target.kind !== "shim" || injected) {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
injected = true;
|
|
2187
|
+
writeFileSync(shimPath, foreignShim);
|
|
2188
|
+
});
|
|
2189
|
+
await assert.rejects(pluginSetup(), /precondition changed/);
|
|
2190
|
+
assert.equal(injected, true);
|
|
2191
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
2192
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2193
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
1821
2194
|
});
|
|
1822
2195
|
}
|
|
1823
2196
|
finally {
|
|
2197
|
+
resetFailureInjector?.();
|
|
2198
|
+
resetPlatform();
|
|
1824
2199
|
await rm(wd, { recursive: true, force: true });
|
|
1825
2200
|
}
|
|
1826
2201
|
});
|
|
1827
|
-
it("
|
|
1828
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-
|
|
2202
|
+
it("preserves a shim referenced by a Unicode-escaped future hook event", async () => {
|
|
2203
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-future-shim-"));
|
|
2204
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1829
2205
|
try {
|
|
1830
|
-
await
|
|
1831
|
-
await
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2206
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2207
|
+
await withTempCwd(wd, async () => {
|
|
2208
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2209
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2210
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2211
|
+
platform: "win32",
|
|
2212
|
+
codexHomeDir,
|
|
2213
|
+
});
|
|
2214
|
+
const sentinel = "__OMX_ESCAPED_SHIM_COMMAND__";
|
|
2215
|
+
const futureHooks = JSON.stringify({
|
|
2216
|
+
...managed,
|
|
2217
|
+
hooks: {
|
|
2218
|
+
...managed.hooks,
|
|
2219
|
+
FutureEvent: [{ hooks: [{ type: "command", command: sentinel }] }],
|
|
2220
|
+
},
|
|
2221
|
+
}, null, 2).replace(JSON.stringify(sentinel), JSON.stringify(`& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimPath}'`).replace(/\\\\/g, "\\u005c")) + "\n";
|
|
2222
|
+
await writeFile(hooksPath, futureHooks);
|
|
2223
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2224
|
+
await setup({
|
|
2225
|
+
scope: "user",
|
|
2226
|
+
installMode: "plugin",
|
|
2227
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2228
|
+
skipNativeAgentRefresh: true,
|
|
2229
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2230
|
+
});
|
|
2231
|
+
assert.equal(existsSync(shimPath), true);
|
|
2232
|
+
const finalHooks = await readFile(hooksPath, "utf-8");
|
|
2233
|
+
assert.match(finalHooks, /FutureEvent/);
|
|
2234
|
+
assert.match(finalHooks, /\\u005c/);
|
|
2235
|
+
assert.doesNotMatch(finalHooks, /codex-native-hook\.js/);
|
|
2236
|
+
});
|
|
1839
2237
|
});
|
|
1840
2238
|
}
|
|
1841
2239
|
finally {
|
|
2240
|
+
resetPlatform();
|
|
1842
2241
|
await rm(wd, { recursive: true, force: true });
|
|
1843
2242
|
}
|
|
1844
2243
|
});
|
|
1845
|
-
it("
|
|
1846
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-
|
|
2244
|
+
it("preserves a shim referenced through a normalized Windows path alias", async () => {
|
|
2245
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-shim-path-alias-"));
|
|
2246
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1847
2247
|
try {
|
|
1848
2248
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1849
2249
|
await withTempCwd(wd, async () => {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2250
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2251
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2252
|
+
const shimAlias = shimPath
|
|
2253
|
+
.replace(/[\\/]+hooks[\\/]+/i, "\\hooks\\\\.\\")
|
|
2254
|
+
.replace(/omx-native-hook-windows-shim\.ps1$/i, "OMX-NATIVE-HOOK-WINDOWS-SHIM.PS1")
|
|
2255
|
+
.replace(/\\/g, "/");
|
|
2256
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2257
|
+
platform: "win32",
|
|
2258
|
+
codexHomeDir,
|
|
2259
|
+
});
|
|
2260
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
2261
|
+
...managed,
|
|
2262
|
+
hooks: {
|
|
2263
|
+
...managed.hooks,
|
|
2264
|
+
FutureEvent: [{
|
|
2265
|
+
hooks: [{
|
|
2266
|
+
type: "command",
|
|
2267
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimAlias}'`,
|
|
2268
|
+
}],
|
|
2269
|
+
}],
|
|
2270
|
+
},
|
|
2271
|
+
}, null, 2) + "\n");
|
|
2272
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2273
|
+
await setup({
|
|
1855
2274
|
scope: "user",
|
|
1856
|
-
installMode: "
|
|
1857
|
-
|
|
2275
|
+
installMode: "plugin",
|
|
2276
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2277
|
+
skipNativeAgentRefresh: true,
|
|
2278
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
1858
2279
|
});
|
|
1859
|
-
assert.equal(existsSync(
|
|
1860
|
-
assert.
|
|
1861
|
-
assert.equal(existsSync(join(codexHomeDir, "prompts", "executor.md")), true);
|
|
2280
|
+
assert.equal(existsSync(shimPath), true);
|
|
2281
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
1862
2282
|
});
|
|
1863
2283
|
});
|
|
1864
2284
|
}
|
|
1865
2285
|
finally {
|
|
2286
|
+
resetPlatform();
|
|
1866
2287
|
await rm(wd, { recursive: true, force: true });
|
|
1867
2288
|
}
|
|
1868
2289
|
});
|
|
1869
|
-
it("
|
|
1870
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-
|
|
2290
|
+
it("preserves a shim for a distinct absolute -File target", async () => {
|
|
2291
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-quoted-inert-shim-"));
|
|
2292
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1871
2293
|
try {
|
|
1872
2294
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1873
2295
|
await withTempCwd(wd, async () => {
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
|
|
1895
|
-
assert.deepEqual(persisted, {
|
|
2296
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2297
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2298
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2299
|
+
platform: "win32",
|
|
2300
|
+
codexHomeDir,
|
|
2301
|
+
});
|
|
2302
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
2303
|
+
...managed,
|
|
2304
|
+
hooks: {
|
|
2305
|
+
...managed.hooks,
|
|
2306
|
+
FutureEvent: [{
|
|
2307
|
+
hooks: [{
|
|
2308
|
+
type: "command",
|
|
2309
|
+
command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
|
|
2310
|
+
}],
|
|
2311
|
+
}],
|
|
2312
|
+
},
|
|
2313
|
+
}, null, 2) + "\n");
|
|
2314
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2315
|
+
await setup({
|
|
1896
2316
|
scope: "user",
|
|
1897
|
-
installMode: "
|
|
1898
|
-
|
|
2317
|
+
installMode: "plugin",
|
|
2318
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2319
|
+
skipNativeAgentRefresh: true,
|
|
2320
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
1899
2321
|
});
|
|
2322
|
+
assert.equal(existsSync(shimPath), true);
|
|
2323
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
1900
2324
|
});
|
|
1901
2325
|
});
|
|
1902
2326
|
}
|
|
1903
2327
|
finally {
|
|
2328
|
+
resetPlatform();
|
|
1904
2329
|
await rm(wd, { recursive: true, force: true });
|
|
1905
2330
|
}
|
|
1906
2331
|
});
|
|
1907
|
-
it("
|
|
1908
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-
|
|
2332
|
+
it("does not retain a shim for inert future-event metadata", async () => {
|
|
2333
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-future-shim-metadata-"));
|
|
2334
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1909
2335
|
try {
|
|
1910
|
-
await
|
|
1911
|
-
await
|
|
1912
|
-
|
|
2336
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2337
|
+
await withTempCwd(wd, async () => {
|
|
2338
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2339
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2340
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2341
|
+
platform: "win32",
|
|
2342
|
+
codexHomeDir,
|
|
2343
|
+
});
|
|
2344
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
2345
|
+
...managed,
|
|
2346
|
+
hooks: {
|
|
2347
|
+
...managed.hooks,
|
|
2348
|
+
FutureEvent: [
|
|
2349
|
+
{
|
|
2350
|
+
metadata: shimPath,
|
|
2351
|
+
hooks: [{ type: "agent", prompt: shimPath }],
|
|
2352
|
+
},
|
|
2353
|
+
],
|
|
2354
|
+
},
|
|
2355
|
+
}, null, 2) + "\n");
|
|
2356
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2357
|
+
await setup({
|
|
2358
|
+
scope: "user",
|
|
2359
|
+
installMode: "plugin",
|
|
2360
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2361
|
+
skipNativeAgentRefresh: true,
|
|
2362
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2363
|
+
});
|
|
2364
|
+
assert.equal(existsSync(shimPath), false);
|
|
2365
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
2366
|
+
});
|
|
1913
2367
|
});
|
|
1914
2368
|
}
|
|
1915
2369
|
finally {
|
|
2370
|
+
resetPlatform();
|
|
1916
2371
|
await rm(wd, { recursive: true, force: true });
|
|
1917
2372
|
}
|
|
1918
2373
|
});
|
|
1919
|
-
it("
|
|
1920
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
2374
|
+
it("completes Windows native-hook transactions when regular-file fsync reports EPERM", async () => {
|
|
2375
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-fsync-eperm-"));
|
|
2376
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2377
|
+
let syncCalls = 0;
|
|
2378
|
+
const stderr = [];
|
|
2379
|
+
const originalStderrWrite = process.stderr.write;
|
|
2380
|
+
process.stderr.write = ((chunk) => {
|
|
2381
|
+
stderr.push(String(chunk));
|
|
2382
|
+
return true;
|
|
2383
|
+
});
|
|
2384
|
+
const resetSync = setNativeHookTransactionRegularFileSyncForTest(async (platform) => {
|
|
2385
|
+
assert.equal(platform, "win32");
|
|
2386
|
+
syncCalls += 1;
|
|
2387
|
+
throw Object.assign(new Error("EPERM: fsync"), { code: "EPERM" });
|
|
2388
|
+
});
|
|
1921
2389
|
try {
|
|
1922
|
-
await
|
|
1923
|
-
await
|
|
1924
|
-
|
|
1925
|
-
|
|
2390
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2391
|
+
await withTempCwd(wd, async () => {
|
|
2392
|
+
await setup({
|
|
2393
|
+
scope: "user",
|
|
2394
|
+
installMode: "legacy",
|
|
2395
|
+
skipNativeAgentRefresh: true,
|
|
2396
|
+
});
|
|
2397
|
+
assert.ok(syncCalls > 0);
|
|
2398
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), true);
|
|
2399
|
+
assert.equal(existsSync(buildManagedCodexNativeHookWindowsShimPath(codexHomeDir)), true);
|
|
2400
|
+
assert.equal(existsSync(join(codexHomeDir, "config.toml")), true);
|
|
2401
|
+
});
|
|
2402
|
+
assert.equal(stderr.filter((line) => line.includes("native-hook setup")).length, 1);
|
|
1926
2403
|
});
|
|
1927
2404
|
}
|
|
1928
2405
|
finally {
|
|
2406
|
+
process.stderr.write = originalStderrWrite;
|
|
2407
|
+
resetSync();
|
|
2408
|
+
resetPlatform();
|
|
1929
2409
|
await rm(wd, { recursive: true, force: true });
|
|
1930
2410
|
}
|
|
1931
2411
|
});
|
|
1932
|
-
it("
|
|
1933
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
2412
|
+
it("orders Windows plugin-transition mutations as hooks, shim, then config", async () => {
|
|
2413
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-removal-"));
|
|
2414
|
+
const forwardOrder = [];
|
|
2415
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2416
|
+
let resetFailureInjector;
|
|
1934
2417
|
try {
|
|
1935
|
-
await withIsolatedUserHome(wd, async () => {
|
|
2418
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1936
2419
|
await withTempCwd(wd, async () => {
|
|
1937
|
-
|
|
1938
|
-
|
|
2420
|
+
await setup({
|
|
2421
|
+
scope: "user",
|
|
2422
|
+
installMode: "legacy",
|
|
2423
|
+
skipNativeAgentRefresh: true,
|
|
1939
2424
|
});
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
assert.
|
|
1943
|
-
assert.
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2425
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2426
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2427
|
+
assert.equal(existsSync(hooksPath), true);
|
|
2428
|
+
assert.equal(existsSync(shimPath), true);
|
|
2429
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
2430
|
+
if (stage === "before_temp_write")
|
|
2431
|
+
forwardOrder.push(artifact.kind);
|
|
2432
|
+
});
|
|
2433
|
+
await setup({
|
|
2434
|
+
scope: "user",
|
|
2435
|
+
installMode: "plugin",
|
|
2436
|
+
skipNativeAgentRefresh: true,
|
|
2437
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2438
|
+
codexFeaturesProbe: () => [
|
|
2439
|
+
"hooks stable true",
|
|
2440
|
+
"plugin_hooks experimental true",
|
|
2441
|
+
"",
|
|
2442
|
+
].join("\n"),
|
|
2443
|
+
});
|
|
2444
|
+
assert.deepEqual(forwardOrder, ["hooks", "shim", "config"]);
|
|
2445
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2446
|
+
assert.equal(existsSync(shimPath), false);
|
|
2447
|
+
});
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
finally {
|
|
2451
|
+
resetFailureInjector?.();
|
|
2452
|
+
resetPlatform();
|
|
2453
|
+
await rm(wd, { recursive: true, force: true });
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
it("rolls back Windows plugin-transition artifacts in exact reverse order", async () => {
|
|
2457
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-rollback-"));
|
|
2458
|
+
const forwardOrder = [];
|
|
2459
|
+
const restorationOrder = [];
|
|
2460
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2461
|
+
let resetFailureInjector;
|
|
2462
|
+
try {
|
|
2463
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2464
|
+
await withTempCwd(wd, async () => {
|
|
2465
|
+
await setup({
|
|
2466
|
+
scope: "user",
|
|
2467
|
+
installMode: "legacy",
|
|
2468
|
+
skipNativeAgentRefresh: true,
|
|
2469
|
+
});
|
|
2470
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2471
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2472
|
+
const hooksBefore = await readFile(hooksPath);
|
|
2473
|
+
const shimBefore = await readFile(shimPath);
|
|
2474
|
+
const configBefore = await readFile(join(codexHomeDir, "config.toml"));
|
|
2475
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
2476
|
+
if (stage === "before_temp_write")
|
|
2477
|
+
forwardOrder.push(artifact.kind);
|
|
2478
|
+
if (stage === "before_readback" && artifact.kind === "config") {
|
|
2479
|
+
throw new Error("injected Windows config replacement failure");
|
|
2480
|
+
}
|
|
2481
|
+
if (stage === "before_rollback_rename") {
|
|
2482
|
+
restorationOrder.push(artifact.kind);
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
await assert.rejects(setup({
|
|
2486
|
+
scope: "user",
|
|
2487
|
+
installMode: "plugin",
|
|
2488
|
+
skipNativeAgentRefresh: true,
|
|
2489
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2490
|
+
codexFeaturesProbe: () => [
|
|
2491
|
+
"hooks stable true",
|
|
2492
|
+
"plugin_hooks experimental true",
|
|
2493
|
+
"",
|
|
2494
|
+
].join("\n"),
|
|
2495
|
+
}), /rolled back: injected Windows config replacement failure/);
|
|
2496
|
+
assert.deepEqual(forwardOrder, ["hooks", "shim", "config"]);
|
|
2497
|
+
assert.deepEqual(restorationOrder, ["config", "shim", "hooks"]);
|
|
2498
|
+
assert.deepEqual(await readFile(join(codexHomeDir, "config.toml")), configBefore);
|
|
2499
|
+
assert.deepEqual(await readFile(shimPath), shimBefore);
|
|
2500
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2501
|
+
});
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
finally {
|
|
2505
|
+
resetFailureInjector?.();
|
|
2506
|
+
resetPlatform();
|
|
2507
|
+
await rm(wd, { recursive: true, force: true });
|
|
2508
|
+
}
|
|
2509
|
+
});
|
|
2510
|
+
it("preserves foreign native hook enablement when transitioning from plugin fallback", async () => {
|
|
2511
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2512
|
+
try {
|
|
2513
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2514
|
+
await withTempCwd(wd, async () => {
|
|
2515
|
+
await setup({
|
|
2516
|
+
scope: "user",
|
|
2517
|
+
installMode: "plugin",
|
|
2518
|
+
codexFeaturesProbe: () => "hooks stable true\n",
|
|
2519
|
+
});
|
|
2520
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2521
|
+
const existingHooks = JSON.parse(await readFile(hooksPath, "utf-8"));
|
|
2522
|
+
const foreignHookGroup = {
|
|
2523
|
+
hooks: [
|
|
2524
|
+
{
|
|
2525
|
+
type: "command",
|
|
2526
|
+
command: "/usr/bin/python3 /tmp/foreign-hook.py",
|
|
2527
|
+
timeout: 5,
|
|
2528
|
+
},
|
|
2529
|
+
],
|
|
2530
|
+
};
|
|
2531
|
+
const existingRegistrations = existingHooks.hooks ?? {};
|
|
2532
|
+
existingHooks.hooks = {
|
|
2533
|
+
...existingRegistrations,
|
|
2534
|
+
UserPromptSubmit: [
|
|
2535
|
+
foreignHookGroup,
|
|
2536
|
+
...(existingRegistrations.UserPromptSubmit ?? []),
|
|
2537
|
+
],
|
|
2538
|
+
};
|
|
2539
|
+
await writeFile(hooksPath, JSON.stringify(existingHooks, null, 2) + "\n");
|
|
2540
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2541
|
+
const finalHooksContent = await readFile(hooksPath, "utf-8");
|
|
2542
|
+
const config = await readFile(join(codexHomeDir, "config.toml"), "utf-8");
|
|
2543
|
+
assert.match(finalHooksContent, /foreign-hook\.py/);
|
|
2544
|
+
assert.doesNotMatch(finalHooksContent, /codex-native-hook\.js/);
|
|
2545
|
+
assert.match(config, /^plugin_hooks = true$/m);
|
|
2546
|
+
assert.match(config, /^hooks = true$/m);
|
|
2547
|
+
});
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
finally {
|
|
2551
|
+
await rm(wd, { recursive: true, force: true });
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
it("keeps native hooks enabled for pre-existing foreign hooks in plugin-scoped setup", async () => {
|
|
2555
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2556
|
+
try {
|
|
2557
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2558
|
+
await withTempCwd(wd, async () => {
|
|
2559
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2560
|
+
const foreignHookGroup = {
|
|
2561
|
+
hooks: [
|
|
2562
|
+
{
|
|
2563
|
+
type: "command",
|
|
2564
|
+
command: "/usr/bin/python3 /tmp/foreign-hook.py",
|
|
2565
|
+
timeout: 5,
|
|
2566
|
+
},
|
|
2567
|
+
],
|
|
2568
|
+
};
|
|
2569
|
+
await writeFile(hooksPath, JSON.stringify({ hooks: { UserPromptSubmit: [foreignHookGroup] } }, null, 2) + "\n");
|
|
2570
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2571
|
+
const finalHooksContent = await readFile(hooksPath, "utf-8");
|
|
2572
|
+
assert.match(finalHooksContent, /"UserPromptSubmit"/);
|
|
2573
|
+
assert.match(finalHooksContent, /foreign-hook\.py/);
|
|
2574
|
+
assert.doesNotMatch(finalHooksContent, /codex-native-hook\.js/);
|
|
2575
|
+
const config = await readFile(join(codexHomeDir, "config.toml"), "utf-8");
|
|
2576
|
+
assert.match(config, /^plugin_hooks = true$/m);
|
|
2577
|
+
assert.match(config, /^hooks = true$/m, "plugin-scoped setup must keep native hooks enabled for foreign hooks");
|
|
2578
|
+
});
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
finally {
|
|
2582
|
+
await rm(wd, { recursive: true, force: true });
|
|
2583
|
+
}
|
|
2584
|
+
});
|
|
2585
|
+
it("preserves same-key user hook trust state in plugin-scoped setup", async () => {
|
|
2586
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2587
|
+
try {
|
|
2588
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2589
|
+
await withTempCwd(wd, async () => {
|
|
2590
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2591
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2592
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
2593
|
+
hooks: {
|
|
2594
|
+
PostCompact: [
|
|
2595
|
+
{
|
|
2596
|
+
hooks: [
|
|
2597
|
+
{
|
|
2598
|
+
type: "command",
|
|
2599
|
+
command: "/usr/bin/python3 /tmp/user-hook.py",
|
|
2600
|
+
timeout: 5,
|
|
2601
|
+
},
|
|
2602
|
+
],
|
|
2603
|
+
},
|
|
2604
|
+
],
|
|
2605
|
+
},
|
|
2606
|
+
}, null, 2) + "\n");
|
|
2607
|
+
await writeFile(configPath, [
|
|
2608
|
+
'model = "gpt-5.6-sol"',
|
|
2609
|
+
"",
|
|
2610
|
+
`[hooks.state."${hooksPath}:post_compact:0:0"]`,
|
|
2611
|
+
'trusted_hash = "sha256:user"',
|
|
2612
|
+
"enabled = false",
|
|
2613
|
+
"",
|
|
2614
|
+
].join("\n"));
|
|
2615
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2616
|
+
const config = await readFile(configPath, "utf-8");
|
|
2617
|
+
assert.match(config, /^plugin_hooks = true$/m);
|
|
2618
|
+
assert.match(config, /^trusted_hash = "sha256:user"$/m);
|
|
2619
|
+
assert.match(config, /^enabled = false$/m);
|
|
2620
|
+
assert.equal(config
|
|
2621
|
+
.split(/\r?\n/)
|
|
2622
|
+
.filter((line) => line.trim() ===
|
|
2623
|
+
`[hooks.state."${hooksPath}:post_compact:0:0"]`).length, 1, "plugin setup must not duplicate preserved user hook trust state");
|
|
2624
|
+
assert.doesNotThrow(() => parseToml(config));
|
|
2625
|
+
});
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
finally {
|
|
2629
|
+
await rm(wd, { recursive: true, force: true });
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
it("honors persisted project-scoped plugin mode on repeat setup", async () => {
|
|
2633
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2634
|
+
try {
|
|
2635
|
+
await withTempCwd(wd, async () => {
|
|
2636
|
+
await setup({ scope: "project", installMode: "plugin" });
|
|
2637
|
+
const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
|
|
2638
|
+
assert.deepEqual(persisted, {
|
|
2639
|
+
scope: "project",
|
|
2640
|
+
installMode: "plugin",
|
|
2641
|
+
mcpMode: "none",
|
|
2642
|
+
});
|
|
2643
|
+
await setup({ scope: "project" });
|
|
2644
|
+
assert.equal(existsSync(join(wd, ".codex", "skills", "ask", "SKILL.md")), false);
|
|
2645
|
+
assert.equal(existsSync(join(wd, ".codex", "agents", "planner.toml")), true);
|
|
2646
|
+
assert.equal(existsSync(join(wd, ".codex", "prompts", "executor.md")), false);
|
|
2647
|
+
assert.equal(existsSync(join(wd, ".codex", "hooks.json")), false);
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
finally {
|
|
2651
|
+
await rm(wd, { recursive: true, force: true });
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
it("lets explicit project legacy setup clear persisted project plugin mode", async () => {
|
|
2655
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2656
|
+
try {
|
|
2657
|
+
await withTempCwd(wd, async () => {
|
|
2658
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
2659
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project", installMode: "plugin" }));
|
|
2660
|
+
await setup({ scope: "project", installMode: "legacy" });
|
|
2661
|
+
const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
|
|
2662
|
+
assert.deepEqual(persisted, { scope: "project", mcpMode: "none" });
|
|
2663
|
+
assert.equal(existsSync(join(wd, ".codex", "skills", "ask", "SKILL.md")), true);
|
|
2664
|
+
assert.equal(existsSync(join(wd, ".codex", "agents", "planner.toml")), true);
|
|
2665
|
+
assert.equal(existsSync(join(wd, ".codex", "prompts", "executor.md")), true);
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
finally {
|
|
2669
|
+
await rm(wd, { recursive: true, force: true });
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
it("lets explicit user legacy setup override persisted user plugin mode", async () => {
|
|
2673
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2674
|
+
try {
|
|
2675
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2676
|
+
await withTempCwd(wd, async () => {
|
|
2677
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
2678
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "user", installMode: "plugin" }));
|
|
2679
|
+
await setup({ installMode: "legacy" });
|
|
2680
|
+
const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
|
|
2681
|
+
assert.deepEqual(persisted, {
|
|
2682
|
+
scope: "user",
|
|
2683
|
+
installMode: "legacy",
|
|
2684
|
+
mcpMode: "none",
|
|
2685
|
+
});
|
|
2686
|
+
assert.equal(existsSync(join(codexHomeDir, "skills", "ask", "SKILL.md")), true);
|
|
2687
|
+
assert.equal(existsSync(join(codexHomeDir, "agents", "planner.toml")), true);
|
|
2688
|
+
assert.equal(existsSync(join(codexHomeDir, "prompts", "executor.md")), true);
|
|
2689
|
+
});
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
finally {
|
|
2693
|
+
await rm(wd, { recursive: true, force: true });
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
it("dedupes plugin-mode hook trust state when switching user setup back to legacy", async () => {
|
|
2697
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2698
|
+
try {
|
|
2699
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2700
|
+
await withTempCwd(wd, async () => {
|
|
2701
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2702
|
+
await setup({ scope: "user", installMode: "plugin", force: true });
|
|
2703
|
+
const pluginConfig = await readFile(configPath, "utf-8");
|
|
2704
|
+
const staleUnfencedPluginConfig = pluginConfig
|
|
2705
|
+
.split(/\r?\n/)
|
|
2706
|
+
.filter((line) => line.trim() !== "# OMX-owned Codex hook trust state" &&
|
|
2707
|
+
line.trim() !==
|
|
2708
|
+
"# Trusts only setup-managed codex-native-hook.js wrappers." &&
|
|
2709
|
+
line.trim() !== "# End OMX-owned Codex hook trust state")
|
|
2710
|
+
.join("\n");
|
|
2711
|
+
await writeFile(configPath, staleUnfencedPluginConfig);
|
|
2712
|
+
assert.doesNotThrow(() => parseToml(staleUnfencedPluginConfig));
|
|
2713
|
+
await setup({ scope: "user", installMode: "legacy", force: true });
|
|
2714
|
+
const legacyConfig = await readFile(configPath, "utf-8");
|
|
2715
|
+
assert.doesNotThrow(() => parseToml(legacyConfig));
|
|
2716
|
+
assert.equal(legacyConfig
|
|
2717
|
+
.split(/\r?\n/)
|
|
2718
|
+
.filter((line) => line.trim() ===
|
|
2719
|
+
`[hooks.state."${join(codexHomeDir, "hooks.json")}:post_compact:0:0"]`).length, 1, "legacy setup should replace stale plugin-mode hook trust state instead of duplicating it");
|
|
2720
|
+
assert.match(legacyConfig, /# OMX-owned Codex hook trust state[\s\S]*# End OMX-owned Codex hook trust state/);
|
|
2721
|
+
const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
|
|
2722
|
+
assert.deepEqual(persisted, {
|
|
2723
|
+
scope: "user",
|
|
2724
|
+
installMode: "legacy",
|
|
2725
|
+
mcpMode: "none",
|
|
2726
|
+
});
|
|
2727
|
+
});
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
finally {
|
|
2731
|
+
await rm(wd, { recursive: true, force: true });
|
|
2732
|
+
}
|
|
2733
|
+
});
|
|
2734
|
+
it("uses project-scoped plugin hooks when plugin mode is explicitly requested", async () => {
|
|
2735
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2736
|
+
try {
|
|
2737
|
+
await withTempCwd(wd, async () => {
|
|
2738
|
+
await setup({ scope: "project", installMode: "plugin" });
|
|
2739
|
+
await assertProjectPluginModeArtifacts(wd);
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
finally {
|
|
2743
|
+
await rm(wd, { recursive: true, force: true });
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
it("honors persisted project plugin mode on repeat setup", async () => {
|
|
2747
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2748
|
+
try {
|
|
2749
|
+
await withTempCwd(wd, async () => {
|
|
2750
|
+
await setup({ scope: "project", installMode: "plugin" });
|
|
2751
|
+
await setup();
|
|
2752
|
+
await assertProjectPluginModeArtifacts(wd);
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
finally {
|
|
2756
|
+
await rm(wd, { recursive: true, force: true });
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
it("prints plugin-mode next steps without legacy-only claims", async () => {
|
|
2760
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2761
|
+
try {
|
|
2762
|
+
await withIsolatedUserHome(wd, async () => {
|
|
2763
|
+
await withTempCwd(wd, async () => {
|
|
2764
|
+
const pluginOutput = await captureConsoleOutput(async () => {
|
|
2765
|
+
await setup({ scope: "project", installMode: "plugin" });
|
|
2766
|
+
});
|
|
2767
|
+
assert.match(pluginOutput, /Using setup install mode: plugin/);
|
|
2768
|
+
assert.match(pluginOutput, /Plugin-scoped Codex hooks and runtime feature flags refresh complete .*plugin_hooks, goals/);
|
|
2769
|
+
assert.doesNotMatch(pluginOutput, /user-scope skill delivery mode/);
|
|
2770
|
+
assert.doesNotMatch(pluginOutput, /use explicit agent_type when spawning OMX roles/);
|
|
2771
|
+
assert.doesNotMatch(pluginOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
|
|
2772
|
+
assert.doesNotMatch(pluginOutput, /AGENTS keyword routing can also activate them implicitly/);
|
|
2773
|
+
assert.doesNotMatch(pluginOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
|
|
2774
|
+
assert.match(pluginOutput, /Registered Codex marketplace oh-my-codex-local supplies OMX skills and workflow surfaces/);
|
|
2775
|
+
assert.match(pluginOutput, /Browse plugin-provided skills with \/skills/);
|
|
2776
|
+
assert.match(pluginOutput, /Plugin-mode AGENTS\.md defaults provide persistent orchestration guidance; developer_instructions is an optional bootstrap/);
|
|
2777
|
+
const legacyWd = join(wd, "legacy");
|
|
1951
2778
|
await mkdir(legacyWd, { recursive: true });
|
|
1952
2779
|
await withTempCwd(legacyWd, async () => {
|
|
1953
2780
|
const legacyOutput = await captureConsoleOutput(async () => {
|
|
1954
2781
|
await setup({ scope: "user", installMode: "legacy" });
|
|
1955
2782
|
});
|
|
1956
|
-
assert.match(legacyOutput, /Native agent role TOML files written to \.codex\/agents\//);
|
|
1957
|
-
assert.match(legacyOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
|
|
1958
|
-
assert.match(legacyOutput, /AGENTS keyword routing can also activate them implicitly/);
|
|
1959
|
-
assert.match(legacyOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
|
|
2783
|
+
assert.match(legacyOutput, /Native agent role TOML files written to \.codex\/agents\//);
|
|
2784
|
+
assert.match(legacyOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
|
|
2785
|
+
assert.match(legacyOutput, /AGENTS keyword routing can also activate them implicitly/);
|
|
2786
|
+
assert.match(legacyOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
|
|
2787
|
+
});
|
|
2788
|
+
});
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
finally {
|
|
2792
|
+
await rm(wd, { recursive: true, force: true });
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
it("removes legacy user components when plugin mode is selected", async () => {
|
|
2796
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2797
|
+
try {
|
|
2798
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2799
|
+
await withTempCwd(wd, async () => {
|
|
2800
|
+
await setup({ scope: "user", installMode: "legacy" });
|
|
2801
|
+
const askSkillPath = join(codexHomeDir, "skills", "ask", "SKILL.md");
|
|
2802
|
+
const promptPath = join(codexHomeDir, "prompts", "executor.md");
|
|
2803
|
+
const agentPath = join(codexHomeDir, "agents", "planner.toml");
|
|
2804
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2805
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2806
|
+
const agentsMdPath = join(codexHomeDir, "AGENTS.md");
|
|
2807
|
+
assert.equal(existsSync(askSkillPath), true);
|
|
2808
|
+
assert.equal(existsSync(promptPath), true);
|
|
2809
|
+
assert.equal(existsSync(agentPath), true);
|
|
2810
|
+
assert.equal(existsSync(hooksPath), true);
|
|
2811
|
+
assert.equal(existsSync(configPath), true);
|
|
2812
|
+
assert.equal(existsSync(agentsMdPath), true);
|
|
2813
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2814
|
+
assert.equal(existsSync(askSkillPath), false);
|
|
2815
|
+
assert.equal(existsSync(promptPath), false);
|
|
2816
|
+
assert.equal(existsSync(agentPath), true);
|
|
2817
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2818
|
+
assert.equal(existsSync(agentsMdPath), true);
|
|
2819
|
+
const config = await readFile(configPath, "utf-8");
|
|
2820
|
+
assert.match(config, /^plugin_hooks = true$/m);
|
|
2821
|
+
assert.doesNotMatch(config, /^\s*(?:notify)\s*=|^\s*\[mcp_servers[.\]]/m);
|
|
2822
|
+
assert.match(config, /^developer_instructions\s*=/m);
|
|
2823
|
+
assert.match(config, /You have oh-my-codex installed\. AGENTS\.md/);
|
|
2824
|
+
});
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
finally {
|
|
2828
|
+
await rm(wd, { recursive: true, force: true });
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
it("preserves existing AGENTS.md when plugin AGENTS defaults are declined", async () => {
|
|
2832
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2833
|
+
try {
|
|
2834
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2835
|
+
await withTempCwd(wd, async () => {
|
|
2836
|
+
await setup({ scope: "user", installMode: "legacy" });
|
|
2837
|
+
const agentsMdPath = join(codexHomeDir, "AGENTS.md");
|
|
2838
|
+
const before = await readFile(agentsMdPath, "utf-8");
|
|
2839
|
+
assert.match(before, /<!-- omx:generated:agents-md -->/);
|
|
2840
|
+
await setup({
|
|
2841
|
+
scope: "user",
|
|
2842
|
+
installMode: "plugin",
|
|
2843
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2844
|
+
});
|
|
2845
|
+
assert.equal(await readFile(agentsMdPath, "utf-8"), before);
|
|
2846
|
+
});
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
finally {
|
|
2850
|
+
await rm(wd, { recursive: true, force: true });
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
it("repairs existing AGENTS.md during non-interactive plugin force setup", async () => {
|
|
2854
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2855
|
+
try {
|
|
2856
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2857
|
+
await withTempCwd(wd, async () => {
|
|
2858
|
+
await mkdir(codexHomeDir, { recursive: true });
|
|
2859
|
+
const agentsMdPath = join(codexHomeDir, "AGENTS.md");
|
|
2860
|
+
await writeFile(agentsMdPath, "# local instructions\n");
|
|
2861
|
+
await setup({ scope: "user", installMode: "plugin", force: true });
|
|
2862
|
+
const after = await readFile(agentsMdPath, "utf-8");
|
|
2863
|
+
assert.match(after, /<!-- omx:generated:agents-md -->/);
|
|
2864
|
+
assert.match(after, /oh-my-codex - Intelligent Multi-Agent Orchestration/);
|
|
2865
|
+
const backupRoot = join(wd, "home", ".omx", "backups", "setup");
|
|
2866
|
+
const backupRuns = await readdir(backupRoot);
|
|
2867
|
+
assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "AGENTS.md"))), true);
|
|
2868
|
+
});
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
finally {
|
|
2872
|
+
await rm(wd, { recursive: true, force: true });
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
it("archives stale legacy prompts and preserves modified native agents when plugin mode refreshes", async () => {
|
|
2876
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2877
|
+
try {
|
|
2878
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2879
|
+
await withTempCwd(wd, async () => {
|
|
2880
|
+
await setup({ scope: "user", installMode: "legacy" });
|
|
2881
|
+
const promptPath = join(codexHomeDir, "prompts", "executor.md");
|
|
2882
|
+
const agentPath = join(codexHomeDir, "agents", "planner.toml");
|
|
2883
|
+
await writeFile(promptPath, "---\ndescription: stale legacy executor prompt\n---\n\nold executor body\n");
|
|
2884
|
+
const staleAgentToml = [
|
|
2885
|
+
"# oh-my-codex agent: planner",
|
|
2886
|
+
'name = "planner"',
|
|
2887
|
+
'description = "stale legacy generated planner"',
|
|
2888
|
+
'developer_instructions = """old planner body"""',
|
|
2889
|
+
"",
|
|
2890
|
+
].join("\n");
|
|
2891
|
+
await writeFile(agentPath, staleAgentToml);
|
|
2892
|
+
const output = await captureConsoleOutput(async () => {
|
|
2893
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2894
|
+
});
|
|
2895
|
+
assert.equal(existsSync(promptPath), false);
|
|
2896
|
+
assert.equal(existsSync(agentPath), true);
|
|
2897
|
+
assert.equal(await readFile(agentPath, "utf-8"), staleAgentToml);
|
|
2898
|
+
assert.match(output, /Archived and removed .* legacy OMX-managed prompt file/);
|
|
2899
|
+
assert.match(output, /Native agent role refresh complete/);
|
|
2900
|
+
const backupRoot = join(wd, "home", ".omx", "backups", "setup");
|
|
2901
|
+
const backupRuns = await readdir(backupRoot);
|
|
2902
|
+
assert.ok(backupRuns.length > 0);
|
|
2903
|
+
assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "prompts", "executor.md"))), true);
|
|
2904
|
+
assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "agents", "planner.toml"))), false);
|
|
2905
|
+
});
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
finally {
|
|
2909
|
+
await rm(wd, { recursive: true, force: true });
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
it("preserves unmanaged native agent TOMLs with obsolete skill_ref during plugin refresh", async () => {
|
|
2913
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2914
|
+
try {
|
|
2915
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2916
|
+
await withTempCwd(wd, async () => {
|
|
2917
|
+
await setup({ scope: "user", installMode: "legacy" });
|
|
2918
|
+
const customAgentPath = join(codexHomeDir, "agents", "custom-reviewer.toml");
|
|
2919
|
+
const generatedAgentPath = join(codexHomeDir, "agents", "ghost.toml");
|
|
2920
|
+
const customAgentToml = [
|
|
2921
|
+
'name = "custom-reviewer"',
|
|
2922
|
+
'description = "user-managed reviewer"',
|
|
2923
|
+
'skill_ref = "custom-reviewer"',
|
|
2924
|
+
"",
|
|
2925
|
+
].join("\n");
|
|
2926
|
+
await writeFile(customAgentPath, customAgentToml);
|
|
2927
|
+
await writeFile(generatedAgentPath, [
|
|
2928
|
+
"# oh-my-codex agent: ghost",
|
|
2929
|
+
'name = "ghost"',
|
|
2930
|
+
'description = "obsolete generated reviewer"',
|
|
2931
|
+
'skill_ref = "ghost"',
|
|
2932
|
+
"",
|
|
2933
|
+
].join("\n"));
|
|
2934
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2935
|
+
assert.equal(await readFile(customAgentPath, "utf-8"), customAgentToml);
|
|
2936
|
+
assert.equal(existsSync(generatedAgentPath), false);
|
|
2937
|
+
});
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
finally {
|
|
2941
|
+
await rm(wd, { recursive: true, force: true });
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
it("counts plugin cleanup skill directory backups in the setup summary", async () => {
|
|
2945
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2946
|
+
try {
|
|
2947
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2948
|
+
await withTempCwd(wd, async () => {
|
|
2949
|
+
await setup({ scope: "user", installMode: "legacy" });
|
|
2950
|
+
await seedPluginCacheFromInstalledSkills(codexHomeDir);
|
|
2951
|
+
const output = await captureConsoleOutput(async () => {
|
|
2952
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2953
|
+
});
|
|
2954
|
+
const skillsSummary = output.match(/skills: updated=0, unchanged=0, backed_up=(\d+), skipped=0, removed=(\d+)/);
|
|
2955
|
+
assert.notEqual(skillsSummary, null);
|
|
2956
|
+
const backedUp = Number(skillsSummary?.[1]);
|
|
2957
|
+
const removed = Number(skillsSummary?.[2]);
|
|
2958
|
+
assert.ok(backedUp > 0);
|
|
2959
|
+
assert.equal(backedUp, removed);
|
|
2960
|
+
});
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
finally {
|
|
2964
|
+
await rm(wd, { recursive: true, force: true });
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
it("removes matching legacy user skills even when plugin readiness is proven", async () => {
|
|
2968
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2969
|
+
try {
|
|
2970
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2971
|
+
await withTempCwd(wd, async () => {
|
|
2972
|
+
await setup({ scope: "user", installMode: "legacy" });
|
|
2973
|
+
await seedPluginCacheFromInstalledSkills(codexHomeDir);
|
|
2974
|
+
const askSkillDir = join(codexHomeDir, "skills", "ask");
|
|
2975
|
+
const wikiSkillDir = join(codexHomeDir, "skills", "wiki");
|
|
2976
|
+
assert.equal(existsSync(askSkillDir), true);
|
|
2977
|
+
assert.equal(existsSync(wikiSkillDir), true);
|
|
2978
|
+
const outputLines = [];
|
|
2979
|
+
const previousLog = console.log;
|
|
2980
|
+
console.log = (...args) => {
|
|
2981
|
+
outputLines.push(args.join(" "));
|
|
2982
|
+
};
|
|
2983
|
+
try {
|
|
2984
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2985
|
+
}
|
|
2986
|
+
finally {
|
|
2987
|
+
console.log = previousLog;
|
|
2988
|
+
}
|
|
2989
|
+
const setupOutput = outputLines.join("\n");
|
|
2990
|
+
assert.equal(existsSync(askSkillDir), false);
|
|
2991
|
+
assert.equal(existsSync(wikiSkillDir), false);
|
|
2992
|
+
assert.match(setupOutput, /skills: updated=0, unchanged=0, backed_up=\d+, skipped=0, removed=\d+/);
|
|
2993
|
+
const backupSetupRoot = join(wd, "home", ".omx", "backups", "setup");
|
|
2994
|
+
const backupTimestamps = await readdir(backupSetupRoot);
|
|
2995
|
+
assert.equal(backupTimestamps.length, 1);
|
|
2996
|
+
const backupSkillsDir = join(backupSetupRoot, backupTimestamps[0], ".codex", "skills");
|
|
2997
|
+
const backedUpSkillNames = await readdir(backupSkillsDir);
|
|
2998
|
+
assert.ok(backedUpSkillNames.includes("ask"));
|
|
2999
|
+
assert.ok(backedUpSkillNames.includes("wiki"));
|
|
3000
|
+
assert.match(setupOutput, new RegExp(`skills: updated=0, unchanged=0, backed_up=${backedUpSkillNames.length}, skipped=0, removed=${backedUpSkillNames.length}`));
|
|
3001
|
+
});
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
finally {
|
|
3005
|
+
await rm(wd, { recursive: true, force: true });
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
it("preserves customized legacy user skills during plugin cleanup", async () => {
|
|
3009
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
3010
|
+
try {
|
|
3011
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3012
|
+
await withTempCwd(wd, async () => {
|
|
3013
|
+
await setup({ scope: "user", installMode: "legacy" });
|
|
3014
|
+
await seedPluginCacheFromInstalledSkills(codexHomeDir);
|
|
3015
|
+
const askSkillPath = join(codexHomeDir, "skills", "ask", "SKILL.md");
|
|
3016
|
+
const wikiSkillDir = join(codexHomeDir, "skills", "wiki");
|
|
3017
|
+
await writeFile(askSkillPath, "# customized ask\n");
|
|
3018
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
3019
|
+
assert.equal(await readFile(askSkillPath, "utf-8"), "# customized ask\n");
|
|
3020
|
+
assert.equal(existsSync(wikiSkillDir), false);
|
|
3021
|
+
});
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
finally {
|
|
3025
|
+
await rm(wd, { recursive: true, force: true });
|
|
3026
|
+
}
|
|
3027
|
+
});
|
|
3028
|
+
it("fails plugin hook-removal preflight before creating unrelated setup artifacts", async () => {
|
|
3029
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-preflight-"));
|
|
3030
|
+
try {
|
|
3031
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3032
|
+
await withTempCwd(wd, async () => {
|
|
3033
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3034
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3035
|
+
const hooksContent = JSON.stringify({
|
|
3036
|
+
hooks: {
|
|
3037
|
+
SessionStart: [
|
|
3038
|
+
{
|
|
3039
|
+
matcher: "startup|resume|clear",
|
|
3040
|
+
hooks: [
|
|
3041
|
+
{
|
|
3042
|
+
type: "command",
|
|
3043
|
+
command: 'node "/repo/dist/scripts/codex-native-hook.js"',
|
|
3044
|
+
},
|
|
3045
|
+
],
|
|
3046
|
+
},
|
|
3047
|
+
{ hooks: [{ type: "command", command: "echo foreign" }] },
|
|
3048
|
+
],
|
|
3049
|
+
},
|
|
3050
|
+
}, null, 2) + "\n";
|
|
3051
|
+
const configContent = 'model = "foreign-config"\n';
|
|
3052
|
+
await writeFile(hooksPath, hooksContent);
|
|
3053
|
+
await writeFile(configPath, configContent);
|
|
3054
|
+
await assert.rejects(() => setup({
|
|
3055
|
+
scope: "user",
|
|
3056
|
+
installMode: "plugin",
|
|
3057
|
+
mergeAgents: true,
|
|
3058
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
3059
|
+
}), (error) => {
|
|
3060
|
+
assert.equal(error.code, "unsafe_managed_removal");
|
|
3061
|
+
return true;
|
|
3062
|
+
});
|
|
3063
|
+
assert.equal(await readFile(hooksPath, "utf-8"), hooksContent);
|
|
3064
|
+
assert.equal(await readFile(configPath, "utf-8"), configContent);
|
|
3065
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3066
|
+
assert.equal(existsSync(join(codexHomeDir, "prompts")), false);
|
|
3067
|
+
assert.equal(existsSync(join(codexHomeDir, "agents")), false);
|
|
3068
|
+
});
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
finally {
|
|
3072
|
+
await rm(wd, { recursive: true, force: true });
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
it("rejects symlinked Codex transaction ancestors without writing foreign storage", async () => {
|
|
3076
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-symlinked-codex-home-"));
|
|
3077
|
+
try {
|
|
3078
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3079
|
+
await withTempCwd(wd, async () => {
|
|
3080
|
+
const foreignCodexHome = join(wd, "foreign-codex-home");
|
|
3081
|
+
await mkdir(foreignCodexHome);
|
|
3082
|
+
await writeFile(join(foreignCodexHome, "sentinel.txt"), "foreign\n");
|
|
3083
|
+
await rm(codexHomeDir, { recursive: true, force: true });
|
|
3084
|
+
await symlink(foreignCodexHome, codexHomeDir);
|
|
3085
|
+
await assert.rejects(setup({
|
|
3086
|
+
scope: "user",
|
|
3087
|
+
installMode: "legacy",
|
|
3088
|
+
skipNativeAgentRefresh: true,
|
|
3089
|
+
}), /ancestor .*symbolic link/);
|
|
3090
|
+
assert.deepEqual(await readdir(foreignCodexHome), ["sentinel.txt"]);
|
|
3091
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3092
|
+
});
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
finally {
|
|
3096
|
+
await rm(wd, { recursive: true, force: true });
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
it("rejects a symlinked native artifact parent before touching its target", async () => {
|
|
3100
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-symlinked-native-parent-"));
|
|
3101
|
+
try {
|
|
3102
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3103
|
+
await withTempCwd(wd, async () => {
|
|
3104
|
+
const foreignMetadataDir = join(wd, "foreign-native-metadata");
|
|
3105
|
+
await mkdir(foreignMetadataDir);
|
|
3106
|
+
await writeFile(join(foreignMetadataDir, "sentinel.txt"), "foreign\n");
|
|
3107
|
+
await symlink(foreignMetadataDir, join(codexHomeDir, ".omx"));
|
|
3108
|
+
await assert.rejects(setup({
|
|
3109
|
+
scope: "user",
|
|
3110
|
+
installMode: "legacy",
|
|
3111
|
+
skipNativeAgentRefresh: true,
|
|
3112
|
+
}), /ancestor .*symbolic link/);
|
|
3113
|
+
assert.deepEqual(await readdir(foreignMetadataDir), ["sentinel.txt"]);
|
|
3114
|
+
assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
|
|
3115
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3116
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3117
|
+
});
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
finally {
|
|
3121
|
+
await rm(wd, { recursive: true, force: true });
|
|
3122
|
+
}
|
|
3123
|
+
});
|
|
3124
|
+
it("revalidates native artifact parent topology immediately before mutation", async () => {
|
|
3125
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-native-parent-"));
|
|
3126
|
+
let resetFailureInjector;
|
|
3127
|
+
try {
|
|
3128
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3129
|
+
await withTempCwd(wd, async () => {
|
|
3130
|
+
const metadataParent = join(codexHomeDir, ".omx");
|
|
3131
|
+
let injected = false;
|
|
3132
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
3133
|
+
if (stage !== "before_precondition" || injected)
|
|
3134
|
+
return;
|
|
3135
|
+
injected = true;
|
|
3136
|
+
writeFileSync(metadataParent, "foreign\n");
|
|
3137
|
+
});
|
|
3138
|
+
await assert.rejects(setup({
|
|
3139
|
+
scope: "user",
|
|
3140
|
+
installMode: "legacy",
|
|
3141
|
+
skipNativeAgentRefresh: true,
|
|
3142
|
+
}), /ancestor .*not a directory/);
|
|
3143
|
+
assert.equal(injected, true);
|
|
3144
|
+
assert.equal(await readFile(metadataParent, "utf-8"), "foreign\n");
|
|
3145
|
+
assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
|
|
3146
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3147
|
+
});
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
finally {
|
|
3151
|
+
resetFailureInjector?.();
|
|
3152
|
+
await rm(wd, { recursive: true, force: true });
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
it("rejects a modified Windows hook shim during setup preflight", async () => {
|
|
3156
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-shim-"));
|
|
3157
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3158
|
+
try {
|
|
3159
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3160
|
+
await withTempCwd(wd, async () => {
|
|
3161
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3162
|
+
const modifiedShim = "# modified by user\n";
|
|
3163
|
+
await writeFile(shimPath, modifiedShim);
|
|
3164
|
+
await assert.rejects(() => setup({
|
|
3165
|
+
scope: "user",
|
|
3166
|
+
installMode: "legacy",
|
|
3167
|
+
mergeAgents: true,
|
|
3168
|
+
}), /modified Windows native hook shim/);
|
|
3169
|
+
assert.equal(await readFile(shimPath, "utf-8"), modifiedShim);
|
|
3170
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3171
|
+
assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
|
|
3172
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3173
|
+
});
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
finally {
|
|
3177
|
+
resetPlatform();
|
|
3178
|
+
await rm(wd, { recursive: true, force: true });
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
it("writes hooks, notification metadata, then config/trust and rolls all writes back when config replacement fails", async () => {
|
|
3182
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-hook-transaction-"));
|
|
3183
|
+
const writeOrder = [];
|
|
3184
|
+
const resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
3185
|
+
if (stage !== "before_temp_write")
|
|
3186
|
+
return;
|
|
3187
|
+
writeOrder.push(artifact.kind);
|
|
3188
|
+
if (artifact.kind === "config") {
|
|
3189
|
+
throw new Error("injected config replacement failure");
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
try {
|
|
3193
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3194
|
+
await withTempCwd(wd, async () => {
|
|
3195
|
+
await writeFile(join(codexHomeDir, "config.toml"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3196
|
+
await assert.rejects(() => setup({
|
|
3197
|
+
scope: "user",
|
|
3198
|
+
installMode: "legacy",
|
|
3199
|
+
mergeAgents: true,
|
|
3200
|
+
skipNativeAgentRefresh: true,
|
|
3201
|
+
}), /rolled back: injected config replacement failure/);
|
|
3202
|
+
assert.deepEqual(writeOrder, ["hooks", "metadata", "config"]);
|
|
3203
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3204
|
+
assert.equal(await readFile(join(codexHomeDir, "config.toml"), "utf-8"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3205
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false);
|
|
3206
|
+
});
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
finally {
|
|
3210
|
+
resetFailureInjector();
|
|
3211
|
+
await rm(wd, { recursive: true, force: true });
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
3214
|
+
it("writes the Windows shim, hooks, notification metadata, then config/trust and rolls all writes back", async () => {
|
|
3215
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-transaction-"));
|
|
3216
|
+
const writeOrder = [];
|
|
3217
|
+
const resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
3218
|
+
if (stage !== "before_temp_write")
|
|
3219
|
+
return;
|
|
3220
|
+
writeOrder.push(artifact.kind);
|
|
3221
|
+
if (artifact.kind === "config") {
|
|
3222
|
+
throw new Error("injected Windows config replacement failure");
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3226
|
+
try {
|
|
3227
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3228
|
+
await withTempCwd(wd, async () => {
|
|
3229
|
+
await writeFile(join(codexHomeDir, "config.toml"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3230
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3231
|
+
await assert.rejects(() => setup({
|
|
3232
|
+
scope: "user",
|
|
3233
|
+
installMode: "legacy",
|
|
3234
|
+
mergeAgents: true,
|
|
3235
|
+
skipNativeAgentRefresh: true,
|
|
3236
|
+
}), /rolled back: injected Windows config replacement failure/);
|
|
3237
|
+
assert.deepEqual(writeOrder, ["shim", "hooks", "metadata", "config"]);
|
|
3238
|
+
assert.equal(existsSync(shimPath), false);
|
|
3239
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3240
|
+
assert.equal(await readFile(join(codexHomeDir, "config.toml"), "utf-8"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3241
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false);
|
|
3242
|
+
});
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
finally {
|
|
3246
|
+
resetFailureInjector();
|
|
3247
|
+
resetPlatform();
|
|
3248
|
+
await rm(wd, { recursive: true, force: true });
|
|
3249
|
+
}
|
|
3250
|
+
});
|
|
3251
|
+
it("aborts stale native snapshots before setup can write unrelated artifacts", async () => {
|
|
3252
|
+
const originalConfig = Buffer.from('model = "before"\n', "utf-8");
|
|
3253
|
+
const originalHooks = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3254
|
+
const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
|
|
3255
|
+
const foreignConfig = Buffer.from('model = "foreign"\n', "utf-8");
|
|
3256
|
+
const fixtures = [
|
|
3257
|
+
{
|
|
3258
|
+
name: "hooks creation",
|
|
3259
|
+
seedHooks: null,
|
|
3260
|
+
mutate: (hooksPath, _configPath) => writeFileSync(hooksPath, foreignHooks),
|
|
3261
|
+
expectedHooks: foreignHooks,
|
|
3262
|
+
expectedConfig: originalConfig,
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
name: "config modification",
|
|
3266
|
+
seedHooks: originalHooks,
|
|
3267
|
+
mutate: (_hooksPath, configPath) => writeFileSync(configPath, foreignConfig),
|
|
3268
|
+
expectedHooks: originalHooks,
|
|
3269
|
+
expectedConfig: foreignConfig,
|
|
3270
|
+
},
|
|
3271
|
+
{
|
|
3272
|
+
name: "hooks deletion",
|
|
3273
|
+
seedHooks: originalHooks,
|
|
3274
|
+
mutate: (hooksPath, _configPath) => rmSync(hooksPath),
|
|
3275
|
+
expectedHooks: null,
|
|
3276
|
+
expectedConfig: originalConfig,
|
|
3277
|
+
},
|
|
3278
|
+
{
|
|
3279
|
+
name: "config deletion",
|
|
3280
|
+
seedHooks: originalHooks,
|
|
3281
|
+
mutate: (_hooksPath, configPath) => rmSync(configPath),
|
|
3282
|
+
expectedHooks: originalHooks,
|
|
3283
|
+
expectedConfig: null,
|
|
3284
|
+
},
|
|
3285
|
+
];
|
|
3286
|
+
for (const fixture of fixtures) {
|
|
3287
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-native-"));
|
|
3288
|
+
let resetFailureInjector;
|
|
3289
|
+
try {
|
|
3290
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3291
|
+
await withTempCwd(wd, async () => {
|
|
3292
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3293
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3294
|
+
await writeFile(configPath, originalConfig);
|
|
3295
|
+
if (fixture.seedHooks)
|
|
3296
|
+
await writeFile(hooksPath, fixture.seedHooks);
|
|
3297
|
+
let injected = false;
|
|
3298
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
3299
|
+
if (stage !== "before_precondition" || injected)
|
|
3300
|
+
return;
|
|
3301
|
+
injected = true;
|
|
3302
|
+
fixture.mutate(hooksPath, configPath);
|
|
3303
|
+
});
|
|
3304
|
+
await assert.rejects(setup({
|
|
3305
|
+
scope: "user",
|
|
3306
|
+
installMode: "legacy",
|
|
3307
|
+
skipNativeAgentRefresh: true,
|
|
3308
|
+
codexFeaturesProbe: () => null,
|
|
3309
|
+
codexVersionProbe: () => null,
|
|
3310
|
+
}), /precondition changed/);
|
|
3311
|
+
assert.equal(injected, true, fixture.name);
|
|
3312
|
+
if (fixture.expectedConfig === null) {
|
|
3313
|
+
assert.equal(existsSync(configPath), false);
|
|
3314
|
+
}
|
|
3315
|
+
else {
|
|
3316
|
+
assert.deepEqual(await readFile(configPath), fixture.expectedConfig);
|
|
3317
|
+
}
|
|
3318
|
+
if (fixture.expectedHooks === null) {
|
|
3319
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3320
|
+
}
|
|
3321
|
+
else {
|
|
3322
|
+
assert.deepEqual(await readFile(hooksPath), fixture.expectedHooks);
|
|
3323
|
+
}
|
|
3324
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3325
|
+
assert.equal(existsSync(join(codexHomeDir, "agents")), false);
|
|
3326
|
+
});
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
finally {
|
|
3330
|
+
resetFailureInjector?.();
|
|
3331
|
+
await rm(wd, { recursive: true, force: true });
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
});
|
|
3335
|
+
it("aborts when notification metadata changes after planning", async () => {
|
|
3336
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-notify-metadata-"));
|
|
3337
|
+
let resetFailureInjector;
|
|
3338
|
+
try {
|
|
3339
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3340
|
+
await withTempCwd(wd, async () => {
|
|
3341
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3342
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3343
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
3344
|
+
const configBefore = 'notify = ["node", "/tmp/user-notify.js"]\n';
|
|
3345
|
+
const foreignMetadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
3346
|
+
await writeFile(configPath, configBefore);
|
|
3347
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
3348
|
+
let injected = false;
|
|
3349
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3350
|
+
if (stage !== "before_precondition" ||
|
|
3351
|
+
target.kind !== "metadata" ||
|
|
3352
|
+
injected) {
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
injected = true;
|
|
3356
|
+
writeFileSync(metadataPath, foreignMetadata);
|
|
3357
|
+
});
|
|
3358
|
+
await assert.rejects(setup({
|
|
3359
|
+
scope: "user",
|
|
3360
|
+
installMode: "legacy",
|
|
3361
|
+
skipNativeAgentRefresh: true,
|
|
3362
|
+
codexFeaturesProbe: () => null,
|
|
3363
|
+
codexVersionProbe: () => null,
|
|
3364
|
+
}), /precondition changed/);
|
|
3365
|
+
assert.equal(injected, true);
|
|
3366
|
+
assert.deepEqual(await readFile(metadataPath), foreignMetadata);
|
|
3367
|
+
assert.equal(await readFile(configPath, "utf-8"), configBefore);
|
|
3368
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3369
|
+
});
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
finally {
|
|
3373
|
+
resetFailureInjector?.();
|
|
3374
|
+
await rm(wd, { recursive: true, force: true });
|
|
3375
|
+
}
|
|
3376
|
+
});
|
|
3377
|
+
it("aborts when the Windows shim changes after planning", async () => {
|
|
3378
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-windows-shim-"));
|
|
3379
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3380
|
+
let resetFailureInjector;
|
|
3381
|
+
try {
|
|
3382
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3383
|
+
await withTempCwd(wd, async () => {
|
|
3384
|
+
await setup({
|
|
3385
|
+
scope: "user",
|
|
3386
|
+
installMode: "legacy",
|
|
3387
|
+
skipNativeAgentRefresh: true,
|
|
3388
|
+
codexFeaturesProbe: () => null,
|
|
3389
|
+
codexVersionProbe: () => null,
|
|
3390
|
+
});
|
|
3391
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3392
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3393
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3394
|
+
const configBefore = await readFile(configPath);
|
|
3395
|
+
await writeFile(hooksPath, '{"hooks": {}}\n');
|
|
3396
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3397
|
+
const foreignShim = Buffer.from("# foreign shim\n", "utf-8");
|
|
3398
|
+
let injected = false;
|
|
3399
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3400
|
+
if (stage === "before_precondition" &&
|
|
3401
|
+
target.kind === "shim" &&
|
|
3402
|
+
!injected) {
|
|
3403
|
+
injected = true;
|
|
3404
|
+
writeFileSync(shimPath, foreignShim);
|
|
3405
|
+
}
|
|
3406
|
+
});
|
|
3407
|
+
await assert.rejects(setup({
|
|
3408
|
+
scope: "user",
|
|
3409
|
+
installMode: "legacy",
|
|
3410
|
+
skipNativeAgentRefresh: true,
|
|
3411
|
+
codexFeaturesProbe: () => null,
|
|
3412
|
+
codexVersionProbe: () => null,
|
|
3413
|
+
}), /precondition changed/);
|
|
3414
|
+
assert.equal(injected, true);
|
|
3415
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
3416
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3417
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3418
|
+
});
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
finally {
|
|
3422
|
+
resetFailureInjector?.();
|
|
3423
|
+
resetPlatform();
|
|
3424
|
+
await rm(wd, { recursive: true, force: true });
|
|
3425
|
+
}
|
|
3426
|
+
});
|
|
3427
|
+
it("never deletes plugin hooks from a stale snapshot", async () => {
|
|
3428
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-plugin-stale-hooks-"));
|
|
3429
|
+
let resetFailureInjector;
|
|
3430
|
+
try {
|
|
3431
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3432
|
+
await withTempCwd(wd, async () => {
|
|
3433
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
3434
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3435
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3436
|
+
const configBefore = await readFile(configPath);
|
|
3437
|
+
const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
|
|
3438
|
+
let injected = false;
|
|
3439
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
3440
|
+
if (stage !== "before_precondition" || injected)
|
|
3441
|
+
return;
|
|
3442
|
+
injected = true;
|
|
3443
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3444
|
+
});
|
|
3445
|
+
await assert.rejects(setup({
|
|
3446
|
+
scope: "user",
|
|
3447
|
+
installMode: "plugin",
|
|
3448
|
+
pluginAgentsMdPrompt: async () => false,
|
|
3449
|
+
skipNativeAgentRefresh: true,
|
|
3450
|
+
}), /precondition changed/);
|
|
3451
|
+
assert.equal(injected, true);
|
|
3452
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3453
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3454
|
+
});
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
finally {
|
|
3458
|
+
resetFailureInjector?.();
|
|
3459
|
+
await rm(wd, { recursive: true, force: true });
|
|
3460
|
+
}
|
|
3461
|
+
});
|
|
3462
|
+
it("never rolls back a concurrent foreign hook replacement", async () => {
|
|
3463
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-rollback-"));
|
|
3464
|
+
let resetFailureInjector;
|
|
3465
|
+
try {
|
|
3466
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3467
|
+
await withTempCwd(wd, async () => {
|
|
3468
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3469
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3470
|
+
const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3471
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
3472
|
+
const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
|
|
3473
|
+
await writeFile(hooksPath, hooksBefore);
|
|
3474
|
+
await writeFile(configPath, configBefore);
|
|
3475
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3476
|
+
if (stage !== "before_temp_write" || target.kind !== "config")
|
|
3477
|
+
return;
|
|
3478
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3479
|
+
throw new Error("injected config replacement failure after foreign hook write");
|
|
3480
|
+
});
|
|
3481
|
+
await assert.rejects(setup({
|
|
3482
|
+
scope: "user",
|
|
3483
|
+
installMode: "legacy",
|
|
3484
|
+
skipNativeAgentRefresh: true,
|
|
3485
|
+
codexFeaturesProbe: () => null,
|
|
3486
|
+
codexVersionProbe: () => null,
|
|
3487
|
+
}), /rollback failed/);
|
|
3488
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3489
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3490
|
+
});
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
finally {
|
|
3494
|
+
resetFailureInjector?.();
|
|
3495
|
+
await rm(wd, { recursive: true, force: true });
|
|
3496
|
+
}
|
|
3497
|
+
});
|
|
3498
|
+
it("preserves pre-existing native bytes and modes across injected transaction failures", async () => {
|
|
3499
|
+
const failures = [
|
|
3500
|
+
{ stage: "before_backup", kind: "shim" },
|
|
3501
|
+
{ stage: "before_temp_write", kind: "shim" },
|
|
3502
|
+
{ stage: "before_rename", kind: "hooks" },
|
|
3503
|
+
{ stage: "before_temp_write", kind: "metadata" },
|
|
3504
|
+
{ stage: "before_readback", kind: "config" },
|
|
3505
|
+
];
|
|
3506
|
+
for (const failure of failures) {
|
|
3507
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-transaction-rollback-"));
|
|
3508
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3509
|
+
let resetFailureInjector;
|
|
3510
|
+
try {
|
|
3511
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3512
|
+
await withTempCwd(wd, async () => {
|
|
3513
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3514
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3515
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3516
|
+
const shimBefore = Buffer.from(buildManagedCodexNativeHookWindowsShimContent("", {
|
|
3517
|
+
nodePath: "C:\\Historical Node\\node.exe",
|
|
3518
|
+
hookScriptPath: "C:\\Historical Install\\oh-my-codex\\dist\\scripts\\codex-native-hook.js",
|
|
3519
|
+
}), "utf-8");
|
|
3520
|
+
const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3521
|
+
const configBefore = Buffer.from('model = "before"\nnotify = ["node", "/tmp/user-notify.js"]\n', "utf-8");
|
|
3522
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
3523
|
+
await writeFile(shimPath, shimBefore);
|
|
3524
|
+
await writeFile(hooksPath, hooksBefore);
|
|
3525
|
+
await writeFile(configPath, configBefore);
|
|
3526
|
+
await Promise.all([
|
|
3527
|
+
chmod(shimPath, 0o640),
|
|
3528
|
+
chmod(hooksPath, 0o640),
|
|
3529
|
+
chmod(configPath, 0o640),
|
|
3530
|
+
]);
|
|
3531
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3532
|
+
if (stage === failure.stage && target.kind === failure.kind) {
|
|
3533
|
+
throw new Error(`injected ${failure.stage} ${failure.kind} failure`);
|
|
3534
|
+
}
|
|
3535
|
+
});
|
|
3536
|
+
await assert.rejects(setup({
|
|
3537
|
+
scope: "user",
|
|
3538
|
+
installMode: "legacy",
|
|
3539
|
+
skipNativeAgentRefresh: true,
|
|
3540
|
+
codexFeaturesProbe: () => null,
|
|
3541
|
+
codexVersionProbe: () => null,
|
|
3542
|
+
}), /injected .* failure/);
|
|
3543
|
+
assert.deepEqual(await readFile(shimPath), shimBefore, failure.stage);
|
|
3544
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore, failure.stage);
|
|
3545
|
+
assert.deepEqual(await readFile(configPath), configBefore, failure.stage);
|
|
3546
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false, failure.stage);
|
|
3547
|
+
for (const path of [shimPath, hooksPath, configPath]) {
|
|
3548
|
+
assert.equal((await stat(path)).mode & 0o777, 0o640, path);
|
|
3549
|
+
}
|
|
3550
|
+
});
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
finally {
|
|
3554
|
+
resetFailureInjector?.();
|
|
3555
|
+
resetPlatform();
|
|
3556
|
+
await rm(wd, { recursive: true, force: true });
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
});
|
|
3560
|
+
it("fails closed when a managed dispatcher metadata snapshot is not valid UTF-8", async () => {
|
|
3561
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-invalid-notify-metadata-"));
|
|
3562
|
+
try {
|
|
3563
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3564
|
+
await withTempCwd(wd, async () => {
|
|
3565
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3566
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
3567
|
+
const dispatcherPath = join(packageRoot, "dist", "scripts", "notify-dispatcher.js");
|
|
3568
|
+
const configBefore = `notify = ${JSON.stringify([
|
|
3569
|
+
"node",
|
|
3570
|
+
dispatcherPath,
|
|
3571
|
+
"--metadata",
|
|
3572
|
+
metadataPath,
|
|
3573
|
+
])}\n`;
|
|
3574
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
3575
|
+
await writeFile(configPath, configBefore);
|
|
3576
|
+
await writeFile(metadataPath, Buffer.from([0xff]));
|
|
3577
|
+
await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /notification metadata .*invalid UTF-8/);
|
|
3578
|
+
assert.equal(await readFile(configPath, "utf-8"), configBefore);
|
|
3579
|
+
assert.deepEqual(await readFile(metadataPath), Buffer.from([0xff]));
|
|
3580
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3581
|
+
});
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
finally {
|
|
3585
|
+
await rm(wd, { recursive: true, force: true });
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
3588
|
+
it("keeps consulted unchanged notification metadata in the native transaction preconditions", async () => {
|
|
3589
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-notify-precondition-"));
|
|
3590
|
+
let resetFailureInjector;
|
|
3591
|
+
try {
|
|
3592
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3593
|
+
await withTempCwd(wd, async () => {
|
|
3594
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3595
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3596
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
3597
|
+
await writeFile(configPath, 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3598
|
+
await setup({
|
|
3599
|
+
scope: "user",
|
|
3600
|
+
installMode: "legacy",
|
|
3601
|
+
skipNativeAgentRefresh: true,
|
|
3602
|
+
});
|
|
3603
|
+
const configBefore = await readFile(configPath);
|
|
3604
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3605
|
+
const foreignMetadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
3606
|
+
let injected = false;
|
|
3607
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3608
|
+
if (stage === "before_precondition" &&
|
|
3609
|
+
target.kind === "metadata" &&
|
|
3610
|
+
!injected) {
|
|
3611
|
+
injected = true;
|
|
3612
|
+
writeFileSync(metadataPath, foreignMetadata);
|
|
3613
|
+
}
|
|
3614
|
+
});
|
|
3615
|
+
await assert.rejects(setup({
|
|
3616
|
+
scope: "user",
|
|
3617
|
+
installMode: "legacy",
|
|
3618
|
+
skipNativeAgentRefresh: true,
|
|
3619
|
+
}), /precondition changed/);
|
|
3620
|
+
assert.equal(injected, true);
|
|
3621
|
+
assert.deepEqual(await readFile(metadataPath), foreignMetadata);
|
|
3622
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3623
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3624
|
+
});
|
|
3625
|
+
});
|
|
3626
|
+
}
|
|
3627
|
+
finally {
|
|
3628
|
+
resetFailureInjector?.();
|
|
3629
|
+
await rm(wd, { recursive: true, force: true });
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
it("rejects config decisions whose MCP-removal callback changed the planned config snapshot", async () => {
|
|
3633
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-config-decision-"));
|
|
3634
|
+
try {
|
|
3635
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3636
|
+
await withTempCwd(wd, async () => {
|
|
3637
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3638
|
+
const configBefore = '[mcp_servers.omx_state]\ncommand = "node"\n';
|
|
3639
|
+
const foreignConfig = 'model = "foreign"\n';
|
|
3640
|
+
await writeFile(configPath, configBefore);
|
|
3641
|
+
await assert.rejects(setup({
|
|
3642
|
+
scope: "user",
|
|
3643
|
+
installMode: "legacy",
|
|
3644
|
+
skipNativeAgentRefresh: true,
|
|
3645
|
+
firstPartyMcpRemovalPrompt: async () => {
|
|
3646
|
+
await writeFile(configPath, foreignConfig);
|
|
3647
|
+
return false;
|
|
3648
|
+
},
|
|
3649
|
+
}), /precondition changed/);
|
|
3650
|
+
assert.equal(await readFile(configPath, "utf-8"), foreignConfig);
|
|
3651
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3652
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx")), false);
|
|
3653
|
+
});
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
finally {
|
|
3657
|
+
await rm(wd, { recursive: true, force: true });
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
it("fails closed for pre-existing native transaction write temporaries", async () => {
|
|
3661
|
+
const fixtures = ["regular file", "symlink"];
|
|
3662
|
+
for (const fixture of fixtures) {
|
|
3663
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-temp-collision-"));
|
|
3664
|
+
let resetTemporaryPath;
|
|
3665
|
+
try {
|
|
3666
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3667
|
+
await withTempCwd(wd, async () => {
|
|
3668
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3669
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3670
|
+
const collisionPath = join(codexHomeDir, ".hooks-write-collision");
|
|
3671
|
+
const configBefore = 'model = "before"\n';
|
|
3672
|
+
await writeFile(configPath, configBefore);
|
|
3673
|
+
if (fixture === "regular file") {
|
|
3674
|
+
await writeFile(collisionPath, "collision\n");
|
|
3675
|
+
}
|
|
3676
|
+
else {
|
|
3677
|
+
const targetPath = join(wd, "collision-target");
|
|
3678
|
+
await writeFile(targetPath, "collision\n");
|
|
3679
|
+
await symlink(targetPath, collisionPath);
|
|
3680
|
+
}
|
|
3681
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "write"
|
|
3682
|
+
? collisionPath
|
|
3683
|
+
: join(dirname(path), `.${purpose}-unused`));
|
|
3684
|
+
await assert.rejects(setup({
|
|
3685
|
+
scope: "user",
|
|
3686
|
+
installMode: "legacy",
|
|
3687
|
+
skipNativeAgentRefresh: true,
|
|
3688
|
+
}), /EEXIST/);
|
|
3689
|
+
assert.equal(await readFile(collisionPath, "utf-8"), "collision\n");
|
|
3690
|
+
assert.equal(await readFile(configPath, "utf-8"), configBefore);
|
|
3691
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3692
|
+
});
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
finally {
|
|
3696
|
+
resetTemporaryPath?.();
|
|
3697
|
+
await rm(wd, { recursive: true, force: true });
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
});
|
|
3701
|
+
it("fails closed when a non-throwing injector replaces an owned write temporary", async () => {
|
|
3702
|
+
const fixtures = ["regular file", "symlink"];
|
|
3703
|
+
for (const fixture of fixtures) {
|
|
3704
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-write-temp-replacement-"));
|
|
3705
|
+
let resetFailureInjector;
|
|
3706
|
+
let resetTemporaryPath;
|
|
3707
|
+
try {
|
|
3708
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3709
|
+
await withTempCwd(wd, async () => {
|
|
3710
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3711
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3712
|
+
const temporaryPath = join(codexHomeDir, ".hooks-write-owned");
|
|
3713
|
+
const foreignTargetPath = join(wd, "foreign-write-temporary-target");
|
|
3714
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
3715
|
+
const foreignContents = `foreign ${fixture} write temporary\n`;
|
|
3716
|
+
await writeFile(configPath, configBefore);
|
|
3717
|
+
if (fixture === "symlink") {
|
|
3718
|
+
await writeFile(foreignTargetPath, foreignContents);
|
|
3719
|
+
}
|
|
3720
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "write"
|
|
3721
|
+
? temporaryPath
|
|
3722
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
|
|
3723
|
+
let injected = false;
|
|
3724
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3725
|
+
if (stage !== "before_rename" || target.kind !== "hooks")
|
|
3726
|
+
return;
|
|
3727
|
+
injected = true;
|
|
3728
|
+
rmSync(temporaryPath);
|
|
3729
|
+
if (fixture === "regular file") {
|
|
3730
|
+
writeFileSync(temporaryPath, foreignContents);
|
|
3731
|
+
}
|
|
3732
|
+
else {
|
|
3733
|
+
symlinkSync(foreignTargetPath, temporaryPath);
|
|
3734
|
+
}
|
|
3735
|
+
});
|
|
3736
|
+
await assert.rejects(setup({
|
|
3737
|
+
scope: "user",
|
|
3738
|
+
installMode: "legacy",
|
|
3739
|
+
skipNativeAgentRefresh: true,
|
|
3740
|
+
}), /temporary.*manual recovery/);
|
|
3741
|
+
assert.equal(injected, true);
|
|
3742
|
+
const replacementStatus = await lstat(temporaryPath);
|
|
3743
|
+
if (fixture === "regular file") {
|
|
3744
|
+
assert.equal(replacementStatus.isFile(), true);
|
|
3745
|
+
assert.equal(await readFile(temporaryPath, "utf-8"), foreignContents);
|
|
3746
|
+
}
|
|
3747
|
+
else {
|
|
3748
|
+
assert.equal(replacementStatus.isSymbolicLink(), true);
|
|
3749
|
+
assert.equal(await readFile(foreignTargetPath, "utf-8"), foreignContents);
|
|
3750
|
+
}
|
|
3751
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3752
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3753
|
+
});
|
|
3754
|
+
});
|
|
3755
|
+
}
|
|
3756
|
+
finally {
|
|
3757
|
+
resetFailureInjector?.();
|
|
3758
|
+
resetTemporaryPath?.();
|
|
3759
|
+
await rm(wd, { recursive: true, force: true });
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
});
|
|
3763
|
+
it("fails closed when a non-throwing injector replaces an owned staged deletion", async () => {
|
|
3764
|
+
const fixtures = ["regular file", "symlink"];
|
|
3765
|
+
for (const fixture of fixtures) {
|
|
3766
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-staged-delete-replacement-"));
|
|
3767
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3768
|
+
let resetFailureInjector;
|
|
3769
|
+
let resetTemporaryPath;
|
|
3770
|
+
try {
|
|
3771
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3772
|
+
await withTempCwd(wd, async () => {
|
|
3773
|
+
await setup({
|
|
3774
|
+
scope: "user",
|
|
3775
|
+
installMode: "legacy",
|
|
3776
|
+
skipNativeAgentRefresh: true,
|
|
3777
|
+
});
|
|
3778
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3779
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3780
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3781
|
+
const stagedDeletionPath = join(codexHomeDir, ".hooks-delete-owned");
|
|
3782
|
+
const foreignTargetPath = join(wd, "foreign-staged-deletion-target");
|
|
3783
|
+
const shimBefore = await readFile(shimPath);
|
|
3784
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3785
|
+
const configBefore = await readFile(configPath);
|
|
3786
|
+
const foreignContents = `foreign ${fixture} staged deletion\n`;
|
|
3787
|
+
if (fixture === "symlink") {
|
|
3788
|
+
await writeFile(foreignTargetPath, foreignContents);
|
|
3789
|
+
}
|
|
3790
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
|
|
3791
|
+
? stagedDeletionPath
|
|
3792
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
|
|
3793
|
+
let injected = false;
|
|
3794
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3795
|
+
if (stage !== "before_remove" || target.kind !== "hooks")
|
|
3796
|
+
return;
|
|
3797
|
+
injected = true;
|
|
3798
|
+
rmSync(stagedDeletionPath);
|
|
3799
|
+
if (fixture === "regular file") {
|
|
3800
|
+
writeFileSync(stagedDeletionPath, foreignContents);
|
|
3801
|
+
}
|
|
3802
|
+
else {
|
|
3803
|
+
symlinkSync(foreignTargetPath, stagedDeletionPath);
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
await assert.rejects(setup({
|
|
3807
|
+
scope: "user",
|
|
3808
|
+
installMode: "plugin",
|
|
3809
|
+
pluginAgentsMdPrompt: async () => false,
|
|
3810
|
+
skipNativeAgentRefresh: true,
|
|
3811
|
+
}), /staged deletion.*manual recovery/);
|
|
3812
|
+
assert.equal(injected, true);
|
|
3813
|
+
const replacementStatus = await lstat(stagedDeletionPath);
|
|
3814
|
+
if (fixture === "regular file") {
|
|
3815
|
+
assert.equal(replacementStatus.isFile(), true);
|
|
3816
|
+
assert.equal(await readFile(stagedDeletionPath, "utf-8"), foreignContents);
|
|
3817
|
+
}
|
|
3818
|
+
else {
|
|
3819
|
+
assert.equal(replacementStatus.isSymbolicLink(), true);
|
|
3820
|
+
assert.equal(await readFile(foreignTargetPath, "utf-8"), foreignContents);
|
|
3821
|
+
}
|
|
3822
|
+
assert.deepEqual(await readFile(shimPath), shimBefore);
|
|
3823
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3824
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3825
|
+
});
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
finally {
|
|
3829
|
+
resetFailureInjector?.();
|
|
3830
|
+
resetTemporaryPath?.();
|
|
3831
|
+
resetPlatform();
|
|
3832
|
+
await rm(wd, { recursive: true, force: true });
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
});
|
|
3836
|
+
it("rolls back a renamed native hook when post-rename verification fails", async () => {
|
|
3837
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-post-rename-rollback-"));
|
|
3838
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("linux");
|
|
3839
|
+
let resetFailureInjector;
|
|
3840
|
+
try {
|
|
3841
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3842
|
+
await withTempCwd(wd, async () => {
|
|
3843
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3844
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3845
|
+
const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3846
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
3847
|
+
await writeFile(hooksPath, hooksBefore);
|
|
3848
|
+
await writeFile(configPath, configBefore);
|
|
3849
|
+
await Promise.all([chmod(hooksPath, 0o640), chmod(configPath, 0o640)]);
|
|
3850
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3851
|
+
if (stage === "after_rename" && target.kind === "hooks") {
|
|
3852
|
+
throw new Error("injected post-rename verification failure");
|
|
3853
|
+
}
|
|
3854
|
+
});
|
|
3855
|
+
await assert.rejects(setup({
|
|
3856
|
+
scope: "user",
|
|
3857
|
+
installMode: "legacy",
|
|
3858
|
+
skipNativeAgentRefresh: true,
|
|
3859
|
+
codexFeaturesProbe: () => null,
|
|
3860
|
+
codexVersionProbe: () => null,
|
|
3861
|
+
}), /rolled back: injected post-rename verification failure/);
|
|
3862
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3863
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3864
|
+
assert.equal((await stat(hooksPath)).mode & 0o777, 0o640);
|
|
3865
|
+
assert.equal((await stat(configPath)).mode & 0o777, 0o640);
|
|
3866
|
+
});
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
finally {
|
|
3870
|
+
resetFailureInjector?.();
|
|
3871
|
+
resetPlatform();
|
|
3872
|
+
await rm(wd, { recursive: true, force: true });
|
|
3873
|
+
}
|
|
3874
|
+
});
|
|
3875
|
+
it("preserves a concurrent replacement after staged source removal for manual recovery", async () => {
|
|
3876
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-post-remove-manual-recovery-"));
|
|
3877
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("linux");
|
|
3878
|
+
let resetFailureInjector;
|
|
3879
|
+
let resetTemporaryPath;
|
|
3880
|
+
try {
|
|
3881
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3882
|
+
await withTempCwd(wd, async () => {
|
|
3883
|
+
await setup({
|
|
3884
|
+
scope: "user",
|
|
3885
|
+
installMode: "legacy",
|
|
3886
|
+
skipNativeAgentRefresh: true,
|
|
3887
|
+
});
|
|
3888
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3889
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3890
|
+
const stagedDeletionPath = join(codexHomeDir, ".hooks-delete-post-remove");
|
|
3891
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3892
|
+
const configBefore = await readFile(configPath);
|
|
3893
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
3894
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
|
|
3895
|
+
? stagedDeletionPath
|
|
3896
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
|
|
3897
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3898
|
+
if (stage === "after_remove" && target.kind === "hooks") {
|
|
3899
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3900
|
+
throw new Error("injected post-remove verification failure");
|
|
3901
|
+
}
|
|
3902
|
+
});
|
|
3903
|
+
await assert.rejects(setup({
|
|
3904
|
+
scope: "user",
|
|
3905
|
+
installMode: "plugin",
|
|
3906
|
+
skipNativeAgentRefresh: true,
|
|
3907
|
+
pluginAgentsMdPrompt: async () => false,
|
|
3908
|
+
codexFeaturesProbe: () => [
|
|
3909
|
+
"hooks stable true",
|
|
3910
|
+
"plugin_hooks experimental true",
|
|
3911
|
+
"",
|
|
3912
|
+
].join("\n"),
|
|
3913
|
+
}), (error) => {
|
|
3914
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3915
|
+
assert.match(message, /manual recovery/);
|
|
3916
|
+
assert.ok(message.includes(hooksPath));
|
|
3917
|
+
return true;
|
|
1960
3918
|
});
|
|
3919
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3920
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3921
|
+
assert.deepEqual(await readFile(stagedDeletionPath), hooksBefore);
|
|
1961
3922
|
});
|
|
1962
3923
|
});
|
|
1963
3924
|
}
|
|
1964
3925
|
finally {
|
|
3926
|
+
resetFailureInjector?.();
|
|
3927
|
+
resetTemporaryPath?.();
|
|
3928
|
+
resetPlatform();
|
|
1965
3929
|
await rm(wd, { recursive: true, force: true });
|
|
1966
3930
|
}
|
|
1967
3931
|
});
|
|
1968
|
-
it("removes
|
|
1969
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
3932
|
+
it("never removes a concurrent replacement while rolling back a newly created artifact", async () => {
|
|
3933
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-remove-"));
|
|
3934
|
+
let resetFailureInjector;
|
|
1970
3935
|
try {
|
|
1971
3936
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1972
3937
|
await withTempCwd(wd, async () => {
|
|
1973
|
-
await setup({ scope: "user", installMode: "legacy" });
|
|
1974
|
-
const askSkillPath = join(codexHomeDir, "skills", "ask", "SKILL.md");
|
|
1975
|
-
const promptPath = join(codexHomeDir, "prompts", "executor.md");
|
|
1976
|
-
const agentPath = join(codexHomeDir, "agents", "planner.toml");
|
|
1977
3938
|
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
1978
3939
|
const configPath = join(codexHomeDir, "config.toml");
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
assert.
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
assert.
|
|
1996
|
-
assert.
|
|
3940
|
+
const configBefore = Buffer.from('notify = ["node", "/tmp/user-notify.js"]\n', "utf-8");
|
|
3941
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
3942
|
+
await writeFile(configPath, configBefore);
|
|
3943
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3944
|
+
if (stage === "before_temp_write" && target.kind === "config") {
|
|
3945
|
+
throw new Error("injected config failure");
|
|
3946
|
+
}
|
|
3947
|
+
if (stage === "before_rollback_remove" && target.kind === "hooks") {
|
|
3948
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3949
|
+
}
|
|
3950
|
+
});
|
|
3951
|
+
await assert.rejects(setup({
|
|
3952
|
+
scope: "user",
|
|
3953
|
+
installMode: "legacy",
|
|
3954
|
+
skipNativeAgentRefresh: true,
|
|
3955
|
+
}), /rollback failed/);
|
|
3956
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3957
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
1997
3958
|
});
|
|
1998
3959
|
});
|
|
1999
3960
|
}
|
|
2000
3961
|
finally {
|
|
3962
|
+
resetFailureInjector?.();
|
|
2001
3963
|
await rm(wd, { recursive: true, force: true });
|
|
2002
3964
|
}
|
|
2003
3965
|
});
|
|
2004
|
-
it("
|
|
2005
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
3966
|
+
it("does not roll back when the second staged-deletion copy drifts after the first cleanup", async () => {
|
|
3967
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-staged-deletion-cleanup-"));
|
|
3968
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3969
|
+
let resetFailureInjector;
|
|
3970
|
+
let resetTemporaryPath;
|
|
2006
3971
|
try {
|
|
2007
3972
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2008
3973
|
await withTempCwd(wd, async () => {
|
|
2009
|
-
await setup({ scope: "user", installMode: "legacy" });
|
|
2010
|
-
const agentsMdPath = join(codexHomeDir, "AGENTS.md");
|
|
2011
|
-
const before = await readFile(agentsMdPath, "utf-8");
|
|
2012
|
-
assert.match(before, /<!-- omx:generated:agents-md -->/);
|
|
2013
3974
|
await setup({
|
|
3975
|
+
scope: "user",
|
|
3976
|
+
installMode: "legacy",
|
|
3977
|
+
skipNativeAgentRefresh: true,
|
|
3978
|
+
});
|
|
3979
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3980
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3981
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3982
|
+
const stagedDeletionPath = (path, purpose) => join(dirname(path), `.${basename(path)}.cleanup-${purpose}`);
|
|
3983
|
+
const foreignStagedCopy = Buffer.from("foreign second staged deletion\n", "utf-8");
|
|
3984
|
+
let cleanupCount = 0;
|
|
3985
|
+
let driftedStagedPath;
|
|
3986
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest(stagedDeletionPath);
|
|
3987
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3988
|
+
if (stage !== "before_staged_cleanup" ||
|
|
3989
|
+
(target.kind !== "shim" && target.kind !== "hooks")) {
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
cleanupCount += 1;
|
|
3993
|
+
if (cleanupCount === 2) {
|
|
3994
|
+
driftedStagedPath = stagedDeletionPath(target.path, "delete");
|
|
3995
|
+
writeFileSync(driftedStagedPath, foreignStagedCopy);
|
|
3996
|
+
}
|
|
3997
|
+
});
|
|
3998
|
+
await assert.rejects(setup({
|
|
2014
3999
|
scope: "user",
|
|
2015
4000
|
installMode: "plugin",
|
|
4001
|
+
skipNativeAgentRefresh: true,
|
|
2016
4002
|
pluginAgentsMdPrompt: async () => false,
|
|
4003
|
+
}), /committed but staged deletion cleanup failed/);
|
|
4004
|
+
assert.equal(cleanupCount, 2);
|
|
4005
|
+
assert.ok(driftedStagedPath);
|
|
4006
|
+
assert.deepEqual(await readFile(driftedStagedPath), foreignStagedCopy);
|
|
4007
|
+
assert.equal(existsSync(shimPath), false);
|
|
4008
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4009
|
+
assert.match(await readFile(configPath, "utf-8"), /^plugin_hooks = true$/m);
|
|
4010
|
+
});
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
finally {
|
|
4014
|
+
resetFailureInjector?.();
|
|
4015
|
+
resetTemporaryPath?.();
|
|
4016
|
+
resetPlatform();
|
|
4017
|
+
await rm(wd, { recursive: true, force: true });
|
|
4018
|
+
}
|
|
4019
|
+
});
|
|
4020
|
+
it("treats applied hook, shim, and config snapshots as CAS through commit finalization", async () => {
|
|
4021
|
+
const fixtures = [
|
|
4022
|
+
{
|
|
4023
|
+
name: "later commit",
|
|
4024
|
+
stage: "before_temp_write",
|
|
4025
|
+
target: "shim",
|
|
4026
|
+
drift: "hooks",
|
|
4027
|
+
committed: false,
|
|
4028
|
+
},
|
|
4029
|
+
{
|
|
4030
|
+
name: "staged cleanup",
|
|
4031
|
+
stage: "before_staged_cleanup",
|
|
4032
|
+
target: "hooks",
|
|
4033
|
+
drift: "shim",
|
|
4034
|
+
committed: false,
|
|
4035
|
+
},
|
|
4036
|
+
{
|
|
4037
|
+
name: "finalization",
|
|
4038
|
+
stage: "after_staged_cleanup",
|
|
4039
|
+
target: "config",
|
|
4040
|
+
drift: "config",
|
|
4041
|
+
committed: true,
|
|
4042
|
+
},
|
|
4043
|
+
];
|
|
4044
|
+
for (const fixture of fixtures) {
|
|
4045
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-setup-applied-${fixture.name}-`));
|
|
4046
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
4047
|
+
let resetFailureInjector;
|
|
4048
|
+
try {
|
|
4049
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4050
|
+
await withTempCwd(wd, async () => {
|
|
4051
|
+
await setup({
|
|
4052
|
+
scope: "user",
|
|
4053
|
+
installMode: "legacy",
|
|
4054
|
+
skipNativeAgentRefresh: true,
|
|
4055
|
+
});
|
|
4056
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
4057
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4058
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4059
|
+
const before = {
|
|
4060
|
+
shim: await readFile(shimPath),
|
|
4061
|
+
hooks: await readFile(hooksPath),
|
|
4062
|
+
config: await readFile(configPath),
|
|
4063
|
+
};
|
|
4064
|
+
const paths = { shim: shimPath, hooks: hooksPath, config: configPath };
|
|
4065
|
+
const foreign = Buffer.from(`foreign applied ${fixture.drift} ${fixture.name}\n`, "utf-8");
|
|
4066
|
+
let injected = false;
|
|
4067
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4068
|
+
if (stage !== fixture.stage ||
|
|
4069
|
+
target.kind !== fixture.target ||
|
|
4070
|
+
injected) {
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
injected = true;
|
|
4074
|
+
writeFileSync(paths[fixture.drift], foreign);
|
|
4075
|
+
});
|
|
4076
|
+
await assert.rejects(setup({
|
|
4077
|
+
scope: "user",
|
|
4078
|
+
installMode: "plugin",
|
|
4079
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4080
|
+
skipNativeAgentRefresh: true,
|
|
4081
|
+
}), fixture.committed
|
|
4082
|
+
? /committed but staged deletion cleanup failed during finalization/
|
|
4083
|
+
: /rollback failed/);
|
|
4084
|
+
assert.equal(injected, true, fixture.name);
|
|
4085
|
+
if (fixture.name === "staged cleanup") {
|
|
4086
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4087
|
+
assert.deepEqual(await readFile(shimPath), foreign);
|
|
4088
|
+
assert.notDeepEqual(await readFile(configPath), before.config);
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
if (fixture.committed) {
|
|
4092
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4093
|
+
assert.equal(existsSync(shimPath), false);
|
|
4094
|
+
assert.deepEqual(await readFile(configPath), foreign);
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
4097
|
+
assert.deepEqual(await readFile(hooksPath), fixture.drift === "hooks" ? foreign : before.hooks);
|
|
4098
|
+
assert.deepEqual(await readFile(shimPath), before.shim);
|
|
4099
|
+
assert.deepEqual(await readFile(configPath), before.config);
|
|
2017
4100
|
});
|
|
2018
|
-
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
finally {
|
|
4104
|
+
resetFailureInjector?.();
|
|
4105
|
+
resetPlatform();
|
|
4106
|
+
await rm(wd, { recursive: true, force: true });
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
});
|
|
4110
|
+
it("preserves a same-byte foreign replacement made immediately after a native-hook rename", async () => {
|
|
4111
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-immediate-post-rename-"));
|
|
4112
|
+
let resetFailureInjector;
|
|
4113
|
+
try {
|
|
4114
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4115
|
+
await withTempCwd(wd, async () => {
|
|
4116
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4117
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4118
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
4119
|
+
let replacement;
|
|
4120
|
+
let replacedInode;
|
|
4121
|
+
await writeFile(configPath, configBefore);
|
|
4122
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4123
|
+
if (stage !== "after_rename" || target.kind !== "hooks")
|
|
4124
|
+
return;
|
|
4125
|
+
replacement = readFileSync(hooksPath);
|
|
4126
|
+
const ownedInode = lstatSync(hooksPath).ino;
|
|
4127
|
+
const foreignPath = join(codexHomeDir, ".foreign-hooks-same-byte");
|
|
4128
|
+
writeFileSync(foreignPath, replacement);
|
|
4129
|
+
assert.notEqual(lstatSync(foreignPath).ino, ownedInode);
|
|
4130
|
+
rmSync(hooksPath);
|
|
4131
|
+
renameSync(foreignPath, hooksPath);
|
|
4132
|
+
replacedInode = lstatSync(hooksPath).ino;
|
|
4133
|
+
assert.notEqual(replacedInode, ownedInode);
|
|
4134
|
+
});
|
|
4135
|
+
await assert.rejects(setup({
|
|
4136
|
+
scope: "user",
|
|
4137
|
+
installMode: "legacy",
|
|
4138
|
+
skipNativeAgentRefresh: true,
|
|
4139
|
+
codexFeaturesProbe: () => null,
|
|
4140
|
+
codexVersionProbe: () => null,
|
|
4141
|
+
}), /rollback failed/);
|
|
4142
|
+
assert.ok(replacement);
|
|
4143
|
+
assert.ok(replacedInode);
|
|
4144
|
+
assert.deepEqual(await readFile(hooksPath), replacement);
|
|
4145
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
2019
4146
|
});
|
|
2020
4147
|
});
|
|
2021
4148
|
}
|
|
2022
4149
|
finally {
|
|
4150
|
+
resetFailureInjector?.();
|
|
2023
4151
|
await rm(wd, { recursive: true, force: true });
|
|
2024
4152
|
}
|
|
2025
4153
|
});
|
|
2026
|
-
it("
|
|
2027
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
4154
|
+
it("preflights every staged native-hook recovery copy before rollback", async () => {
|
|
4155
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-recovery-preflight-"));
|
|
4156
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
4157
|
+
let resetFailureInjector;
|
|
4158
|
+
let resetTemporaryPath;
|
|
2028
4159
|
try {
|
|
2029
4160
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2030
4161
|
await withTempCwd(wd, async () => {
|
|
2031
|
-
await
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
4162
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
4163
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4164
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
4165
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4166
|
+
const configBefore = await readFile(configPath);
|
|
4167
|
+
const stagedHooksPath = join(codexHomeDir, ".hooks-preflight-recovery");
|
|
4168
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
|
|
4169
|
+
? stagedHooksPath
|
|
4170
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-preflight`));
|
|
4171
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4172
|
+
if (stage !== "before_readback" || target.kind !== "config")
|
|
4173
|
+
return;
|
|
4174
|
+
rmSync(stagedHooksPath);
|
|
4175
|
+
throw new Error("injected missing staged recovery copy");
|
|
4176
|
+
});
|
|
4177
|
+
await assert.rejects(setup({
|
|
4178
|
+
scope: "user",
|
|
4179
|
+
installMode: "plugin",
|
|
4180
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4181
|
+
skipNativeAgentRefresh: true,
|
|
4182
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
4183
|
+
}), /rollback failed.*recovery preflight/);
|
|
4184
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4185
|
+
assert.equal(existsSync(shimPath), false);
|
|
4186
|
+
assert.notDeepEqual(await readFile(configPath), configBefore);
|
|
4187
|
+
assert.equal(existsSync(stagedHooksPath), false);
|
|
2041
4188
|
});
|
|
2042
4189
|
});
|
|
2043
4190
|
}
|
|
2044
4191
|
finally {
|
|
4192
|
+
resetFailureInjector?.();
|
|
4193
|
+
resetTemporaryPath?.();
|
|
4194
|
+
resetPlatform();
|
|
2045
4195
|
await rm(wd, { recursive: true, force: true });
|
|
2046
4196
|
}
|
|
2047
4197
|
});
|
|
2048
|
-
it("
|
|
2049
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
4198
|
+
it("stops later native-hook rollback restores when the first restored artifact drifts", async () => {
|
|
4199
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-restored-drift-"));
|
|
4200
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
4201
|
+
let resetFailureInjector;
|
|
2050
4202
|
try {
|
|
2051
4203
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2052
4204
|
await withTempCwd(wd, async () => {
|
|
2053
|
-
await setup({ scope: "user", installMode: "legacy" });
|
|
2054
|
-
const
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
4205
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
4206
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4207
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
4208
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4209
|
+
const configBefore = await readFile(configPath);
|
|
4210
|
+
let injectedFailure = false;
|
|
4211
|
+
let drifted = false;
|
|
4212
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4213
|
+
if (stage === "before_readback" && target.kind === "config" && !injectedFailure) {
|
|
4214
|
+
injectedFailure = true;
|
|
4215
|
+
throw new Error("injected rollback start failure");
|
|
4216
|
+
}
|
|
4217
|
+
if (stage === "before_rollback_rename" && target.kind === "shim" && !drifted) {
|
|
4218
|
+
const restoredInode = lstatSync(configPath).ino;
|
|
4219
|
+
const foreignPath = join(codexHomeDir, ".foreign-restored-config");
|
|
4220
|
+
writeFileSync(foreignPath, configBefore);
|
|
4221
|
+
assert.notEqual(lstatSync(foreignPath).ino, restoredInode);
|
|
4222
|
+
rmSync(configPath);
|
|
4223
|
+
renameSync(foreignPath, configPath);
|
|
4224
|
+
drifted = lstatSync(configPath).ino !== restoredInode;
|
|
4225
|
+
}
|
|
2067
4226
|
});
|
|
2068
|
-
assert.
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
assert.
|
|
2076
|
-
assert.equal(
|
|
2077
|
-
assert.
|
|
4227
|
+
await assert.rejects(setup({
|
|
4228
|
+
scope: "user",
|
|
4229
|
+
installMode: "plugin",
|
|
4230
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4231
|
+
skipNativeAgentRefresh: true,
|
|
4232
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
4233
|
+
}), /rollback failed/);
|
|
4234
|
+
assert.equal(injectedFailure, true);
|
|
4235
|
+
assert.equal(drifted, true);
|
|
4236
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4237
|
+
assert.equal(existsSync(shimPath), false);
|
|
4238
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2078
4239
|
});
|
|
2079
4240
|
});
|
|
2080
4241
|
}
|
|
2081
4242
|
finally {
|
|
4243
|
+
resetFailureInjector?.();
|
|
4244
|
+
resetPlatform();
|
|
2082
4245
|
await rm(wd, { recursive: true, force: true });
|
|
2083
4246
|
}
|
|
2084
4247
|
});
|
|
2085
|
-
it("preserves
|
|
2086
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
4248
|
+
it("preserves a foreign inode injected after final setup rename validation", async () => {
|
|
4249
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-rename-claim-"));
|
|
4250
|
+
let resetFailureInjector;
|
|
2087
4251
|
try {
|
|
2088
4252
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2089
4253
|
await withTempCwd(wd, async () => {
|
|
2090
|
-
|
|
2091
|
-
const
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
assert.equal(
|
|
2109
|
-
assert.
|
|
4254
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4255
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4256
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
4257
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
4258
|
+
await writeFile(configPath, configBefore);
|
|
4259
|
+
await writeFile(hooksPath, '{"hooks": {}}\n');
|
|
4260
|
+
let foreignInode;
|
|
4261
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4262
|
+
if (stage !== "after_final_rename_validation" || target.kind !== "hooks")
|
|
4263
|
+
return;
|
|
4264
|
+
const foreignPath = join(codexHomeDir, ".foreign-final-rename-hooks");
|
|
4265
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
4266
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
4267
|
+
rmSync(hooksPath);
|
|
4268
|
+
renameSync(foreignPath, hooksPath);
|
|
4269
|
+
});
|
|
4270
|
+
await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /precondition changed/);
|
|
4271
|
+
assert.ok(foreignInode);
|
|
4272
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
4273
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
4274
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
2110
4275
|
});
|
|
2111
4276
|
});
|
|
2112
4277
|
}
|
|
2113
4278
|
finally {
|
|
4279
|
+
resetFailureInjector?.();
|
|
2114
4280
|
await rm(wd, { recursive: true, force: true });
|
|
2115
4281
|
}
|
|
2116
4282
|
});
|
|
2117
|
-
it("
|
|
2118
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
4283
|
+
it("preserves a foreign inode injected after final setup removal validation", async () => {
|
|
4284
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-remove-claim-"));
|
|
4285
|
+
let resetFailureInjector;
|
|
2119
4286
|
try {
|
|
2120
4287
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2121
4288
|
await withTempCwd(wd, async () => {
|
|
2122
|
-
await setup({ scope: "user", installMode: "legacy" });
|
|
2123
|
-
|
|
2124
|
-
const
|
|
2125
|
-
|
|
4289
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
4290
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4291
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4292
|
+
const configBefore = await readFile(configPath);
|
|
4293
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
4294
|
+
let foreignInode;
|
|
4295
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4296
|
+
if (stage !== "after_final_remove_validation" || target.kind !== "hooks")
|
|
4297
|
+
return;
|
|
4298
|
+
const foreignPath = join(codexHomeDir, ".foreign-final-remove-hooks");
|
|
4299
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
4300
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
4301
|
+
rmSync(hooksPath);
|
|
4302
|
+
renameSync(foreignPath, hooksPath);
|
|
2126
4303
|
});
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
4304
|
+
await assert.rejects(setup({
|
|
4305
|
+
scope: "user",
|
|
4306
|
+
installMode: "plugin",
|
|
4307
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4308
|
+
skipNativeAgentRefresh: true,
|
|
4309
|
+
}), /precondition changed/);
|
|
4310
|
+
assert.ok(foreignInode);
|
|
4311
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
4312
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
4313
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
2133
4314
|
});
|
|
2134
4315
|
});
|
|
2135
4316
|
}
|
|
2136
4317
|
finally {
|
|
4318
|
+
resetFailureInjector?.();
|
|
2137
4319
|
await rm(wd, { recursive: true, force: true });
|
|
2138
4320
|
}
|
|
2139
4321
|
});
|
|
2140
|
-
it("
|
|
2141
|
-
const wd = await mkdtemp(join(tmpdir(), "omx-setup-
|
|
4322
|
+
it("preserves a foreign inode injected after final setup rollback validation", async () => {
|
|
4323
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-restore-claim-"));
|
|
4324
|
+
let resetFailureInjector;
|
|
2142
4325
|
try {
|
|
2143
4326
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2144
4327
|
await withTempCwd(wd, async () => {
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
assert.
|
|
2164
|
-
assert.
|
|
2165
|
-
assert.
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
assert.equal(backupTimestamps.length, 1);
|
|
2169
|
-
const backupSkillsDir = join(backupSetupRoot, backupTimestamps[0], ".codex", "skills");
|
|
2170
|
-
const backedUpSkillNames = await readdir(backupSkillsDir);
|
|
2171
|
-
assert.ok(backedUpSkillNames.includes("ask"));
|
|
2172
|
-
assert.ok(backedUpSkillNames.includes("wiki"));
|
|
2173
|
-
assert.match(setupOutput, new RegExp(`skills: updated=0, unchanged=0, backed_up=${backedUpSkillNames.length}, skipped=0, removed=${backedUpSkillNames.length}`));
|
|
4328
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4329
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4330
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
4331
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
4332
|
+
await writeFile(configPath, configBefore);
|
|
4333
|
+
let foreignInode;
|
|
4334
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4335
|
+
if (stage === "before_temp_write" && target.kind === "config") {
|
|
4336
|
+
throw new Error("injected rollback start");
|
|
4337
|
+
}
|
|
4338
|
+
if (stage !== "after_final_restore_validation" || target.kind !== "hooks")
|
|
4339
|
+
return;
|
|
4340
|
+
const foreignPath = join(codexHomeDir, ".foreign-final-restore-hooks");
|
|
4341
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
4342
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
4343
|
+
rmSync(hooksPath);
|
|
4344
|
+
renameSync(foreignPath, hooksPath);
|
|
4345
|
+
});
|
|
4346
|
+
await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /rollback failed/);
|
|
4347
|
+
assert.ok(foreignInode);
|
|
4348
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
4349
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
4350
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
2174
4351
|
});
|
|
2175
4352
|
});
|
|
2176
4353
|
}
|
|
2177
4354
|
finally {
|
|
4355
|
+
resetFailureInjector?.();
|
|
2178
4356
|
await rm(wd, { recursive: true, force: true });
|
|
2179
4357
|
}
|
|
2180
4358
|
});
|
|
2181
|
-
|
|
2182
|
-
|
|
4359
|
+
});
|
|
4360
|
+
describe("late setup failure transaction boundary", () => {
|
|
4361
|
+
it("does not commit config or hooks when a later setup phase fails", async () => {
|
|
4362
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-late-failure-"));
|
|
4363
|
+
let resetLateFailure;
|
|
2183
4364
|
try {
|
|
2184
4365
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2185
4366
|
await withTempCwd(wd, async () => {
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
const
|
|
2189
|
-
const
|
|
2190
|
-
await
|
|
2191
|
-
await
|
|
2192
|
-
|
|
2193
|
-
|
|
4367
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4368
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4369
|
+
const hooksBefore = Buffer.from('{"hooks":{"FutureEvent":[{"hooks":[{"type":"prompt","prompt":"keep"}]}]}}\n');
|
|
4370
|
+
const configBefore = Buffer.from('[features]\nhooks = true\n');
|
|
4371
|
+
await mkdir(codexHomeDir, { recursive: true });
|
|
4372
|
+
await writeFile(hooksPath, hooksBefore);
|
|
4373
|
+
await writeFile(configPath, configBefore);
|
|
4374
|
+
resetLateFailure = setSetupLatePhaseFailureInjectorForTest(() => {
|
|
4375
|
+
throw new Error("injected late setup phase failure");
|
|
4376
|
+
});
|
|
4377
|
+
await assert.rejects(setup({
|
|
4378
|
+
scope: "user",
|
|
4379
|
+
installMode: "legacy",
|
|
4380
|
+
skipNativeAgentRefresh: true,
|
|
4381
|
+
codexFeaturesProbe: () => null,
|
|
4382
|
+
codexVersionProbe: () => null,
|
|
4383
|
+
}), /injected late setup phase failure/);
|
|
4384
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
4385
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4386
|
+
});
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
finally {
|
|
4390
|
+
resetLateFailure?.();
|
|
4391
|
+
await rm(wd, { recursive: true, force: true });
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
});
|
|
4395
|
+
describe("persisted merge policy lifecycle", () => {
|
|
4396
|
+
it("preserves matching policy through review and lets explicit sets override reset or a scope change", async () => {
|
|
4397
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-lifecycle-"));
|
|
4398
|
+
try {
|
|
4399
|
+
await withIsolatedUserHome(wd, async () => {
|
|
4400
|
+
await withTempCwd(wd, async () => {
|
|
4401
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
4402
|
+
const statePath = join(wd, ".omx", "setup-scope.json");
|
|
4403
|
+
await writeFile(statePath, JSON.stringify({ scope: "user", installMode: "legacy", mergeAgents: true }));
|
|
4404
|
+
await setup({ persistedSetupReviewPrompt: async () => "review", setupScopePrompt: async () => "user", installModePrompt: async () => "legacy" });
|
|
4405
|
+
assert.equal(JSON.parse(await readFile(statePath, "utf-8")).mergeAgents, true);
|
|
4406
|
+
await setup({ persistedSetupReviewPrompt: async () => "reset", scope: "user", mergeAgents: false });
|
|
4407
|
+
assert.equal(JSON.parse(await readFile(statePath, "utf-8")).mergeAgents, false);
|
|
4408
|
+
await setup({ persistedSetupReviewPrompt: async () => "review", scope: "project", installMode: "legacy", mergeAgents: true });
|
|
4409
|
+
const changedScope = JSON.parse(await readFile(statePath, "utf-8"));
|
|
4410
|
+
assert.equal(changedScope.scope, "project");
|
|
4411
|
+
assert.equal(changedScope.mergeAgents, true);
|
|
4412
|
+
});
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
finally {
|
|
4416
|
+
await rm(wd, { recursive: true, force: true });
|
|
4417
|
+
}
|
|
4418
|
+
});
|
|
4419
|
+
it("removes an explicit policy when clear succeeds", async () => {
|
|
4420
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-clear-"));
|
|
4421
|
+
try {
|
|
4422
|
+
await withIsolatedUserHome(wd, async () => {
|
|
4423
|
+
await withTempCwd(wd, async () => {
|
|
4424
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
4425
|
+
const statePath = join(wd, ".omx", "setup-scope.json");
|
|
4426
|
+
await writeFile(statePath, JSON.stringify({ scope: "project", mergeAgents: true }));
|
|
4427
|
+
await setup({
|
|
4428
|
+
scope: "project",
|
|
4429
|
+
installMode: "legacy",
|
|
4430
|
+
mergeAgentsPolicy: { kind: "clear" },
|
|
4431
|
+
});
|
|
4432
|
+
assert.equal(Object.hasOwn(JSON.parse(await readFile(statePath, "utf-8")), "mergeAgents"), false);
|
|
4433
|
+
});
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
finally {
|
|
4437
|
+
await rm(wd, { recursive: true, force: true });
|
|
4438
|
+
}
|
|
4439
|
+
});
|
|
4440
|
+
it("covers persisted and explicit merge policy precedence across force", async () => {
|
|
4441
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-force-"));
|
|
4442
|
+
try {
|
|
4443
|
+
await withIsolatedUserHome(wd, async () => {
|
|
4444
|
+
await withTempCwd(wd, async () => {
|
|
4445
|
+
const statePath = join(wd, ".omx", "setup-scope.json");
|
|
4446
|
+
const agentsPath = join(wd, "AGENTS.md");
|
|
4447
|
+
const rows = [
|
|
4448
|
+
{ name: "persisted true + force", stored: true, policy: undefined, keepsCustom: true, persisted: true, refresh: "--merge-agents" },
|
|
4449
|
+
{ name: "persisted false + force", stored: false, policy: undefined, keepsCustom: false, persisted: false, refresh: "--no-merge-agents" },
|
|
4450
|
+
{ name: "absent + force", stored: undefined, policy: undefined, keepsCustom: false, persisted: undefined, refresh: undefined },
|
|
4451
|
+
{ name: "force + explicit true", stored: false, policy: { kind: "set", value: true }, keepsCustom: true, persisted: true, refresh: "--merge-agents" },
|
|
4452
|
+
{ name: "force + explicit false", stored: true, policy: { kind: "set", value: false }, keepsCustom: false, persisted: false, refresh: "--no-merge-agents" },
|
|
4453
|
+
{ name: "force + explicit clear", stored: true, policy: { kind: "clear" }, keepsCustom: false, persisted: undefined, refresh: undefined },
|
|
4454
|
+
];
|
|
4455
|
+
for (const row of rows) {
|
|
4456
|
+
await rm(join(wd, ".omx"), { recursive: true, force: true });
|
|
4457
|
+
await rm(join(wd, ".codex"), { recursive: true, force: true });
|
|
4458
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
4459
|
+
await writeFile(statePath, JSON.stringify({ scope: "project", ...(row.stored === undefined ? {} : { mergeAgents: row.stored }) }));
|
|
4460
|
+
await writeFile(agentsPath, `# custom ${row.name}\n`);
|
|
4461
|
+
await setup({
|
|
4462
|
+
scope: "project",
|
|
4463
|
+
installMode: "legacy",
|
|
4464
|
+
force: true,
|
|
4465
|
+
mergeAgentsPolicy: row.policy,
|
|
4466
|
+
});
|
|
4467
|
+
const agents = await readFile(agentsPath, "utf-8");
|
|
4468
|
+
assert.equal(agents.includes(`# custom ${row.name}`), row.keepsCustom, row.name);
|
|
4469
|
+
const persisted = JSON.parse(await readFile(statePath, "utf-8"));
|
|
4470
|
+
assert.equal(persisted.mergeAgents, row.persisted, row.name);
|
|
4471
|
+
assert.equal(Object.hasOwn(persisted, "force"), false, row.name);
|
|
4472
|
+
const refreshArgs = resolveSetupRefreshArgs(wd);
|
|
4473
|
+
assert.equal(refreshArgs.includes("--force"), false, row.name);
|
|
4474
|
+
if (row.refresh)
|
|
4475
|
+
assert.equal(refreshArgs.includes(row.refresh), true, row.name);
|
|
4476
|
+
else
|
|
4477
|
+
assert.equal(refreshArgs.some((arg) => arg === "--merge-agents" || arg === "--no-merge-agents"), false, row.name);
|
|
4478
|
+
}
|
|
2194
4479
|
});
|
|
2195
4480
|
});
|
|
2196
4481
|
}
|