@selesai/code 0.13.15 → 0.13.18
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/CHANGELOG.md +37 -0
- package/README.md +2 -14
- package/dist/bun/cli.d.ts +3 -1
- package/dist/bun/cli.js +3 -9
- package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
- package/dist/bun/sandbox-env-setup.d.ts +1 -0
- package/dist/bun/sandbox-env-setup.js +3 -0
- package/dist/cli/auth-command.d.ts +23 -0
- package/dist/cli/auth-command.js +102 -0
- package/dist/cli/config-selector.js +2 -1
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +11 -0
- package/dist/cli.js +2 -14
- package/dist/config.d.ts +3 -2
- package/dist/config.js +9 -4
- package/dist/core/agent-session-runtime.js +15 -7
- package/dist/core/agent-session.d.ts +2 -6
- package/dist/core/agent-session.js +15 -4
- package/dist/core/compaction/branch-summarization.d.ts +1 -1
- package/dist/core/compaction/branch-summarization.js +2 -1
- package/dist/core/extensions/loader.js +3 -0
- package/dist/core/extensions/types.d.ts +16 -2
- package/dist/core/http-dispatcher.js +2 -0
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.js +4 -0
- package/dist/core/messages.d.ts +1 -1
- package/dist/core/model-config.d.ts +15 -0
- package/dist/core/model-config.js +3 -0
- package/dist/core/model-runtime.d.ts +1 -0
- package/dist/core/model-runtime.js +5 -2
- package/dist/core/provider-composer.js +8 -2
- package/dist/core/session-export.d.ts +3 -0
- package/dist/core/session-export.js +31 -0
- package/dist/core/session-manager.d.ts +3 -2
- package/dist/core/session-manager.js +40 -13
- package/dist/core/skills.d.ts +1 -1
- package/dist/core/skills.js +4 -2
- package/dist/core/system-prompt.js +10 -10
- package/dist/core/tools/bash.js +3 -117
- package/dist/core/tools/edit.d.ts +1 -12
- package/dist/core/tools/edit.js +5 -163
- package/dist/core/tools/find.js +4 -55
- package/dist/core/tools/grep.js +4 -60
- package/dist/core/tools/ls.js +4 -49
- package/dist/core/tools/read.d.ts +4 -0
- package/dist/core/tools/read.js +10 -122
- package/dist/core/tools/renderers/bash.d.ts +11 -0
- package/dist/core/tools/renderers/bash.js +129 -0
- package/dist/core/tools/renderers/edit.d.ts +22 -0
- package/dist/core/tools/renderers/edit.js +166 -0
- package/dist/core/tools/renderers/find.d.ts +9 -0
- package/dist/core/tools/renderers/find.js +63 -0
- package/dist/core/tools/renderers/grep.d.ts +9 -0
- package/dist/core/tools/renderers/grep.js +68 -0
- package/dist/core/tools/renderers/index.d.ts +27 -0
- package/dist/core/tools/renderers/index.js +46 -0
- package/dist/core/tools/renderers/ls.d.ts +9 -0
- package/dist/core/tools/renderers/ls.js +57 -0
- package/dist/core/tools/renderers/read.d.ts +10 -0
- package/dist/core/tools/renderers/read.js +128 -0
- package/dist/core/tools/renderers/write.d.ts +9 -0
- package/dist/core/tools/renderers/write.js +152 -0
- package/dist/core/tools/write.js +5 -146
- package/dist/extensions/capability-gateway/index.ts +12 -1
- package/dist/extensions/capability-gateway/integration.test.ts +23 -0
- package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
- package/dist/extensions/pi-hermes-memory/README.md +16 -12
- package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
- package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
- package/dist/extensions/pi-hermes-memory/package.json +4 -4
- package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
- package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
- package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
- package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
- package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
- package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
- package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
- package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
- package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
- package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
- package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
- package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
- package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
- package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
- package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
- package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
- package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
- package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
- package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
- package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +16 -2
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
- package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
- package/dist/extensions/pi-intercom/index.ts +1 -1
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
- package/dist/extensions/pi-intercom/project-agent.ts +1 -6
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
- package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
- package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
- package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
- package/dist/extensions/pi-subagents/README.md +3 -3
- package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
- package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/docs/agents.md +32 -16
- package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
- package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
- package/dist/extensions/pi-subagents/docs/missions.md +2 -0
- package/dist/extensions/pi-subagents/docs/models.md +58 -1
- package/dist/extensions/pi-subagents/docs/observability.md +50 -12
- package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
- package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
- package/dist/extensions/pi-subagents/package-lock.json +747 -177
- package/dist/extensions/pi-subagents/package.json +6 -4
- package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
- package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
- package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
- package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
- package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
- package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
- package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
- package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
- package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
- package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
- package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
- package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
- package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
- package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
- package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
- package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
- package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
- package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
- package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
- package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
- package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
- package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
- package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
- package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
- package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
- package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
- package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
- package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
- package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
- package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
- package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
- package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
- package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
- package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
- package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
- package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
- package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
- package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
- package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
- package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
- package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
- package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
- package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
- package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
- package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
- package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
- package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
- package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
- package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
- package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
- package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
- package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
- package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
- package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
- package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
- package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
- package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
- package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
- package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
- package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
- package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
- package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
- package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
- package/dist/extensions/pi-web-agent/src/types.ts +1 -1
- package/dist/extensions/pi-zentui/README.md +17 -0
- package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
- package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
- package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
- package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
- package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
- package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
- package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
- package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
- package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
- package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
- package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
- package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
- package/dist/extensions/pi-zentui/package-lock.json +277 -517
- package/dist/extensions/pi-zentui/package.json +7 -4
- package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
- package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
- package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
- package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
- package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
- package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
- package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
- package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
- package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
- package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
- package/dist/index.d.ts +1 -1
- package/dist/main.js +4 -1
- package/dist/modes/interactive/chat-viewport.d.ts +19 -0
- package/dist/modes/interactive/chat-viewport.js +27 -0
- package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
- package/dist/modes/interactive/components/assistant-message.js +30 -13
- package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
- package/dist/modes/interactive/components/custom-editor.js +39 -1
- package/dist/modes/interactive/components/first-time-setup.js +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
- package/dist/modes/interactive/components/markdown-transform.js +18 -0
- package/dist/modes/interactive/components/model-selector.js +19 -17
- package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
- package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -20
- package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
- package/dist/modes/interactive/components/settings-submenu.js +164 -0
- package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
- package/dist/modes/interactive/components/status-indicator.js +10 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.js +75 -18
- package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
- package/dist/modes/interactive/components/tool-execution.js +35 -33
- package/dist/modes/interactive/components/trust-selector.js +2 -2
- package/dist/modes/interactive/interactive-mode.d.ts +13 -15
- package/dist/modes/interactive/interactive-mode.js +144 -80
- package/dist/modes/interactive/session-share.d.ts +15 -0
- package/dist/modes/interactive/session-share.js +183 -0
- package/dist/modes/interactive/theme/dark.json +2 -1
- package/dist/modes/interactive/theme/light.json +2 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
- package/dist/modes/interactive/theme/theme-controller.js +5 -0
- package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
- package/dist/modes/interactive/theme/theme-json.js +129 -0
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive/theme/theme.d.ts +14 -5
- package/dist/modes/interactive/theme/theme.js +18 -119
- package/dist/modes/interactive/tui-renderer.d.ts +20 -0
- package/dist/modes/interactive/tui-renderer.js +65 -0
- package/dist/utils/exif-orientation.js +2 -3
- package/dist/utils/management-http.d.ts +24 -0
- package/dist/utils/management-http.js +53 -0
- package/dist/utils/text.d.ts +7 -0
- package/dist/utils/text.js +8 -0
- package/dist/utils/tools-manager.d.ts +1 -0
- package/dist/utils/tools-manager.js +43 -21
- package/docs/docs.json +0 -4
- package/package.json +4 -4
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
- package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
- package/dist/skills/implanger/SKILL.md +0 -70
- package/dist/skills/output/SKILL.md +0 -49
- package/dist/skills/planger/SKILL.md +0 -167
- package/dist/skills/workflow/SKILL.md +0 -95
- package/docs/workflows.md +0 -18
- /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { describe, it } from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
7
9
|
import {
|
|
8
10
|
cleanupWorktrees,
|
|
9
11
|
createWorktrees,
|
|
@@ -11,11 +13,13 @@ import {
|
|
|
11
13
|
findWorktreeTaskCwdConflict,
|
|
12
14
|
formatWorktreeDiffSummary,
|
|
13
15
|
buildWorktreeNaming,
|
|
16
|
+
normalizeWorktreeBaseRef,
|
|
14
17
|
normalizeWorktreeBranchPrefix,
|
|
15
18
|
resolveExpectedWorktreeAgentCwd,
|
|
16
19
|
resolveWorktreeProvider,
|
|
17
20
|
sanitizeWorktreePathComponent,
|
|
18
21
|
shouldDeferWorktreeCwd,
|
|
22
|
+
WorktreeSetupError,
|
|
19
23
|
type WorktreeSetup,
|
|
20
24
|
} from "../../src/runs/shared/worktree.ts";
|
|
21
25
|
|
|
@@ -37,13 +41,21 @@ function createRepo(prefix: string): string {
|
|
|
37
41
|
fs.writeFileSync(path.join(repoDir, "tracked.txt"), "initial\n", "utf-8");
|
|
38
42
|
git(repoDir, ["add", "-A"]);
|
|
39
43
|
git(repoDir, ["commit", "-m", "initial commit"]);
|
|
40
|
-
|
|
44
|
+
// Match Git's canonical top-level path for macOS symlinked temp roots.
|
|
45
|
+
return fs.realpathSync(repoDir);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
function cleanupRepo(repoDir: string): void {
|
|
48
|
+
function cleanupRepo(repoDir: string, baseDir?: string): void {
|
|
49
|
+
cleanupProjectWorktrees(repoDir, baseDir);
|
|
44
50
|
try { fs.rmSync(repoDir, { recursive: true, force: true }); } catch {}
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/** Removes only this repo's nested project folder; never the shared dedicated root. */
|
|
54
|
+
function cleanupProjectWorktrees(repoDir: string, baseDir?: string): void {
|
|
55
|
+
const dedicatedRoot = baseDir ?? path.join(path.dirname(repoDir), "worktrees");
|
|
56
|
+
try { fs.rmSync(path.join(dedicatedRoot, path.basename(repoDir)), { recursive: true, force: true }); } catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
function createHookScript(_repoDir: string, fileName: string, source: string): string {
|
|
48
60
|
const hooksDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-hook-script-"));
|
|
49
61
|
const hookPath = path.join(hooksDir, fileName);
|
|
@@ -59,12 +71,41 @@ const worktrunkShimSkip = process.platform === "win32"
|
|
|
59
71
|
? "Windows Terminal installs a wt.exe app alias that can outrank test command shims."
|
|
60
72
|
: undefined;
|
|
61
73
|
|
|
74
|
+
// Unknown settlement intentionally poisons the process. Re-run only that case in
|
|
75
|
+
// an awaited real child so subsequent tests retain normal mutation admission.
|
|
76
|
+
async function runPoisonCaseInChild(name: string): Promise<boolean> {
|
|
77
|
+
if (process.env.PI_WORKTREE_POISON_CASE === name) return false;
|
|
78
|
+
await promisify(execFile)(process.execPath, [
|
|
79
|
+
"--experimental-strip-types", "--import", "./test/support/register-loader.mjs",
|
|
80
|
+
"--test", `--test-name-pattern=${name}`, fileURLToPath(import.meta.url),
|
|
81
|
+
], { env: { ...process.env, NODE_TEST_CONTEXT: undefined, PI_WORKTREE_POISON_CASE: name } });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertRetainedHookFailure(error: unknown, message: RegExp): true {
|
|
86
|
+
assert.ok(error instanceof WorktreeSetupError);
|
|
87
|
+
assert.match(error.message, message);
|
|
88
|
+
assert.match(error.message, /manual reconciliation required/i);
|
|
89
|
+
assert.ok(error.snapshot.unknown);
|
|
90
|
+
assert.equal(error.snapshot.cleanup?.state, "partial");
|
|
91
|
+
assert.equal(error.snapshot.cleanup?.pruned, false);
|
|
92
|
+
const worktree = error.snapshot.setup.worktrees[0]!;
|
|
93
|
+
assert.ok(worktree);
|
|
94
|
+
assert.equal(error.snapshot.attempts[0]?.validated, true);
|
|
95
|
+
assert.equal(fs.existsSync(worktree.path), true);
|
|
96
|
+
assert.equal(git(worktree.path, ["branch", "--show-current"]), worktree.branch);
|
|
97
|
+
assert.equal(fs.readFileSync(path.join(worktree.path, "tracked.txt"), "utf-8"), "initial\n");
|
|
98
|
+
assert.throws(() => cleanupWorktrees(error.snapshot.setup, { kind: "setup-rollback" }), /Worktree mutation blocked/i);
|
|
99
|
+
assert.equal(fs.existsSync(worktree.path), true);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
62
103
|
describe("worktree", () => {
|
|
63
|
-
it("createWorktrees returns expected structure", () => {
|
|
104
|
+
it("createWorktrees returns expected structure", async () => {
|
|
64
105
|
const repoDir = createRepo("pi-worktree-structure-");
|
|
65
106
|
let setup: WorktreeSetup | undefined;
|
|
66
107
|
try {
|
|
67
|
-
setup = createWorktrees(repoDir, "structure", 2, { provider: "native" });
|
|
108
|
+
setup = await createWorktrees(repoDir, "structure", 2, { provider: "native" });
|
|
68
109
|
assert.equal(setup.worktrees.length, 2);
|
|
69
110
|
assert.equal(setup.cwd, git(repoDir, ["rev-parse", "--show-toplevel"]));
|
|
70
111
|
for (let i = 0; i < setup.worktrees.length; i++) {
|
|
@@ -82,21 +123,32 @@ describe("worktree", () => {
|
|
|
82
123
|
}
|
|
83
124
|
});
|
|
84
125
|
|
|
85
|
-
it("
|
|
126
|
+
it("reports attempted ownership before spawn and only validated worktrees after allocation", async () => {
|
|
86
127
|
const repoDir = createRepo("pi-worktree-before-create-");
|
|
87
128
|
let setup: WorktreeSetup | undefined;
|
|
88
|
-
let
|
|
129
|
+
let attemptedPath: string | undefined;
|
|
130
|
+
let validated: WorktreeSetup | undefined;
|
|
89
131
|
try {
|
|
90
|
-
setup = createWorktrees(repoDir, "before-create", 1, {
|
|
132
|
+
setup = await createWorktrees(repoDir, "before-create", 1, {
|
|
91
133
|
provider: "native",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
onProgress: (snapshot) => {
|
|
135
|
+
const attempt = snapshot.attempts[0];
|
|
136
|
+
if (attempt && !attempt.command?.pid && !attempt.validated) {
|
|
137
|
+
attemptedPath = attempt.path;
|
|
138
|
+
assert.ok(attemptedPath);
|
|
139
|
+
assert.equal(fs.existsSync(attemptedPath), false);
|
|
140
|
+
assert.equal(attempt.branch, "pi-subagents/task-before-creat-s0-t0");
|
|
141
|
+
assert.deepEqual(snapshot.setup.worktrees, []);
|
|
142
|
+
}
|
|
143
|
+
if (snapshot.setup.worktrees.length) {
|
|
144
|
+
assert.equal(attempt?.validated, true);
|
|
145
|
+
validated = snapshot.setup;
|
|
146
|
+
}
|
|
96
147
|
},
|
|
97
148
|
});
|
|
98
|
-
assert.equal(
|
|
99
|
-
assert.equal(
|
|
149
|
+
assert.equal(attemptedPath, setup.worktrees[0]?.path);
|
|
150
|
+
assert.equal(validated?.baseCommit, setup.baseCommit);
|
|
151
|
+
assert.equal(validated?.worktrees[0]?.path, setup.worktrees[0]?.path);
|
|
100
152
|
assert.equal(fs.existsSync(setup.worktrees[0]!.path), true);
|
|
101
153
|
} finally {
|
|
102
154
|
if (setup) cleanupWorktrees(setup);
|
|
@@ -104,12 +156,39 @@ describe("worktree", () => {
|
|
|
104
156
|
}
|
|
105
157
|
});
|
|
106
158
|
|
|
107
|
-
it("
|
|
159
|
+
it("compensates a rejected ready publication without retaining command output in snapshots", async () => {
|
|
160
|
+
const repoDir = createRepo("pi-worktree-ready-rejected-");
|
|
161
|
+
try {
|
|
162
|
+
await assert.rejects(() => createWorktrees(repoDir, "ready-rejected", 1, {
|
|
163
|
+
provider: "native",
|
|
164
|
+
onProgress: (snapshot) => {
|
|
165
|
+
if (snapshot.command?.result) {
|
|
166
|
+
assert.equal("stdout" in snapshot.command.result, false);
|
|
167
|
+
assert.equal("stderr" in snapshot.command.result, false);
|
|
168
|
+
}
|
|
169
|
+
if (snapshot.phase === "ready") throw new Error("ready publication rejected");
|
|
170
|
+
},
|
|
171
|
+
}), (error: unknown) => {
|
|
172
|
+
assert.ok(error instanceof WorktreeSetupError);
|
|
173
|
+
assert.match(error.message, /ready publication rejected/);
|
|
174
|
+
assert.equal(error.snapshot.cleanup?.state, "complete");
|
|
175
|
+
assert.equal(error.snapshot.cleanup.tasks.length, 1);
|
|
176
|
+
const task = error.snapshot.cleanup.tasks[0]!;
|
|
177
|
+
assert.equal(task.worktreeRemoved, true);
|
|
178
|
+
assert.equal(task.branchRemoved, true);
|
|
179
|
+
assert.equal(fs.existsSync(task.path), false);
|
|
180
|
+
assert.equal(git(repoDir, ["branch", "--list", task.branch]), "");
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
183
|
+
} finally { cleanupRepo(repoDir); }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("records Worktrunk ownership and uses its returned path", async () => {
|
|
108
187
|
try { resolveWorktreeProvider("worktrunk"); } catch { return; }
|
|
109
188
|
const repoDir = createRepo("pi-worktree-worktrunk-");
|
|
110
189
|
let setup: WorktreeSetup | undefined;
|
|
111
190
|
try {
|
|
112
|
-
setup = createWorktrees(repoDir, "worktrunk-s0", 1, {
|
|
191
|
+
setup = await createWorktrees(repoDir, "worktrunk-s0", 1, {
|
|
113
192
|
provider: "worktrunk",
|
|
114
193
|
agents: ["worker"],
|
|
115
194
|
labels: ["Review API"],
|
|
@@ -128,7 +207,8 @@ describe("worktree", () => {
|
|
|
128
207
|
}
|
|
129
208
|
});
|
|
130
209
|
|
|
131
|
-
it("rejects Worktrunk responses that point at the source checkout", { skip: worktrunkShimSkip }, () => {
|
|
210
|
+
it("rejects Worktrunk responses that point at the source checkout", { skip: worktrunkShimSkip }, async () => {
|
|
211
|
+
if (await runPoisonCaseInChild("rejects Worktrunk responses that point at the source checkout")) return;
|
|
132
212
|
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-fake-source-wt-"));
|
|
133
213
|
const fakeScript = path.join(fakeBin, "wt.cjs");
|
|
134
214
|
const fakeWt = path.join(fakeBin, process.platform === "win32" ? "wt.cmd" : "wt");
|
|
@@ -153,16 +233,30 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
153
233
|
const previousPathExt = process.env.PATHEXT;
|
|
154
234
|
const repoDir = createRepo("pi-worktree-source-wt-");
|
|
155
235
|
try {
|
|
156
|
-
const
|
|
236
|
+
const originalHead = git(repoDir, ["rev-parse", "HEAD"]);
|
|
157
237
|
process.env.PATH = `${fakeBin}${path.delimiter}${previousPath ?? ""}`;
|
|
158
238
|
process.env.Path = `${fakeBin}${path.delimiter}${previousWindowsPath ?? previousPath ?? ""}`;
|
|
159
239
|
process.env.PATHEXT = [".CMD", ".EXE", ".BAT", previousPathExt ?? ""].filter(Boolean).join(path.delimiter);
|
|
160
|
-
assert.
|
|
240
|
+
await assert.rejects(
|
|
161
241
|
() => createWorktrees(repoDir, "source-path", 1, { provider: "worktrunk" }),
|
|
162
|
-
|
|
242
|
+
(error: unknown) => {
|
|
243
|
+
assert.ok(error instanceof WorktreeSetupError);
|
|
244
|
+
assert.match(error.message, /source checkout path/i);
|
|
245
|
+
assert.match(error.message, /manual reconciliation required/i);
|
|
246
|
+
assert.ok(error.snapshot.unknown);
|
|
247
|
+
assert.deepEqual(error.snapshot.setup.worktrees, []);
|
|
248
|
+
assert.equal(error.snapshot.attempts[0]?.validated, false);
|
|
249
|
+
assert.equal(error.snapshot.attempts[0]?.branch, "pi-subagents/task-source-path-s0-t0");
|
|
250
|
+
assert.equal(error.snapshot.cleanup?.pruned, false);
|
|
251
|
+
return true;
|
|
252
|
+
},
|
|
163
253
|
);
|
|
164
|
-
|
|
165
|
-
assert.equal(git(repoDir, ["branch", "--
|
|
254
|
+
// Invalid output grants no authority to restore or delete source state.
|
|
255
|
+
assert.equal(git(repoDir, ["branch", "--show-current"]), "pi-subagents/task-source-path-s0-t0");
|
|
256
|
+
assert.equal(git(repoDir, ["rev-parse", "HEAD"]), originalHead);
|
|
257
|
+
assert.equal(fs.readFileSync(path.join(repoDir, "tracked.txt"), "utf-8"), "initial\n");
|
|
258
|
+
await assert.rejects(() => createWorktrees(repoDir, "after-unknown", 1, { provider: "native" }), /unknown|manual reconciliation/i);
|
|
259
|
+
assert.equal(git(repoDir, ["branch", "--list", "pi-subagents/task-after-unknow-s0-t0"]), "");
|
|
166
260
|
} finally {
|
|
167
261
|
if (previousPath === undefined) delete process.env.PATH;
|
|
168
262
|
else process.env.PATH = previousPath;
|
|
@@ -175,7 +269,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
175
269
|
}
|
|
176
270
|
});
|
|
177
271
|
|
|
178
|
-
it("falls back to native only when Worktrunk capability probing fails", () => {
|
|
272
|
+
it("falls back to native only when Worktrunk capability probing fails", async () => {
|
|
179
273
|
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-fake-wt-"));
|
|
180
274
|
const fakeWt = path.join(fakeBin, process.platform === "win32" ? "wt.cmd" : "wt");
|
|
181
275
|
fs.writeFileSync(fakeWt, process.platform === "win32" ? "@echo not-worktrunk\r\n" : "#!/bin/sh\nprintf 'not-worktrunk\\n'\n", "utf-8");
|
|
@@ -193,8 +287,8 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
193
287
|
assert.equal(shouldDeferWorktreeCwd(undefined), true);
|
|
194
288
|
assert.equal(resolveWorktreeProvider(undefined, " "), "native");
|
|
195
289
|
assert.equal(shouldDeferWorktreeCwd(undefined, " "), false);
|
|
196
|
-
assert.
|
|
197
|
-
setup = createWorktrees(repoDir, "provider-fallback", 1);
|
|
290
|
+
await assert.rejects(() => createWorktrees(repoDir, "provider-fallback", 1, { baseDir: " " }), /cannot be empty/i);
|
|
291
|
+
setup = await createWorktrees(repoDir, "provider-fallback", 1);
|
|
198
292
|
assert.equal(setup.worktrees[0]?.provider, "native");
|
|
199
293
|
assert.throws(() => resolveWorktreeProvider("worktrunk"), /Worktrunk provider is unavailable/i);
|
|
200
294
|
} finally {
|
|
@@ -227,7 +321,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
227
321
|
assert.throws(() => normalizeWorktreeBranchPrefix("../unsafe"), /invalid/i);
|
|
228
322
|
});
|
|
229
323
|
|
|
230
|
-
it("createWorktrees maps subdirectory cwd to each agentCwd", () => {
|
|
324
|
+
it("createWorktrees maps subdirectory cwd to each agentCwd", async () => {
|
|
231
325
|
const repoDir = createRepo("pi-worktree-subdir-");
|
|
232
326
|
const nestedDir = path.join(repoDir, "packages", "app");
|
|
233
327
|
fs.mkdirSync(nestedDir, { recursive: true });
|
|
@@ -237,7 +331,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
237
331
|
|
|
238
332
|
let setup: WorktreeSetup | undefined;
|
|
239
333
|
try {
|
|
240
|
-
setup = createWorktrees(nestedDir, "subdir", 1);
|
|
334
|
+
setup = await createWorktrees(nestedDir, "subdir", 1);
|
|
241
335
|
assert.equal(setup.worktrees[0]!.agentCwd, path.join(setup.worktrees[0]!.path, "packages", "app"));
|
|
242
336
|
} finally {
|
|
243
337
|
if (setup) cleanupWorktrees(setup);
|
|
@@ -254,31 +348,41 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
254
348
|
git(repoDir, ["commit", "-m", "add nested dir"]);
|
|
255
349
|
|
|
256
350
|
try {
|
|
351
|
+
const repoRoot = git(nestedDir, ["rev-parse", "--show-toplevel"]);
|
|
257
352
|
assert.equal(
|
|
258
353
|
resolveExpectedWorktreeAgentCwd(nestedDir, "preview", 2),
|
|
259
|
-
path.join(
|
|
354
|
+
path.join(
|
|
355
|
+
path.dirname(repoRoot),
|
|
356
|
+
"worktrees",
|
|
357
|
+
path.basename(repoRoot),
|
|
358
|
+
"pi-worktree-preview-2",
|
|
359
|
+
"packages",
|
|
360
|
+
"app",
|
|
361
|
+
),
|
|
260
362
|
);
|
|
261
363
|
} finally {
|
|
262
364
|
cleanupRepo(repoDir);
|
|
263
365
|
}
|
|
264
366
|
});
|
|
265
367
|
|
|
266
|
-
it("creates worktrees under a configured base directory", () => {
|
|
368
|
+
it("creates worktrees under a configured base directory", async () => {
|
|
267
369
|
const repoDir = createRepo("pi-worktree-base-dir-");
|
|
268
370
|
const baseDir = path.join(os.tmpdir(), `pi-worktree-base-${Date.now().toString(36)}`, "nested");
|
|
269
371
|
let setup: WorktreeSetup | undefined;
|
|
270
372
|
try {
|
|
271
|
-
setup = createWorktrees(repoDir, "base-dir", 1, { baseDir });
|
|
272
|
-
assert.equal(
|
|
373
|
+
setup = await createWorktrees(repoDir, "base-dir", 1, { baseDir });
|
|
374
|
+
assert.equal(
|
|
375
|
+
setup.worktrees[0]!.path,
|
|
376
|
+
path.join(baseDir, path.basename(repoDir), "pi-worktree-base-dir-0"),
|
|
377
|
+
);
|
|
273
378
|
assert.ok(fs.existsSync(baseDir), "configured base directory should be created");
|
|
274
379
|
} finally {
|
|
275
380
|
if (setup) cleanupWorktrees(setup);
|
|
276
|
-
cleanupRepo(repoDir);
|
|
277
|
-
fs.rmSync(path.dirname(baseDir), { recursive: true, force: true });
|
|
381
|
+
cleanupRepo(repoDir, baseDir);
|
|
278
382
|
}
|
|
279
383
|
});
|
|
280
384
|
|
|
281
|
-
it("rejects worktree base directories inside Pi extensions", () => {
|
|
385
|
+
it("rejects worktree base directories inside Pi extensions", async () => {
|
|
282
386
|
const repoDir = createRepo("pi-worktree-extension-dir-");
|
|
283
387
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-home-"));
|
|
284
388
|
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
@@ -289,11 +393,11 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
289
393
|
process.env.HOME = tempHome;
|
|
290
394
|
process.env.USERPROFILE = tempHome;
|
|
291
395
|
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
292
|
-
assert.
|
|
396
|
+
await assert.rejects(
|
|
293
397
|
() => createWorktrees(repoDir, "extension-dir", 1, { baseDir: extensionsDir }),
|
|
294
398
|
/worktree base directory cannot be inside Pi extensions directory/i,
|
|
295
399
|
);
|
|
296
|
-
assert.
|
|
400
|
+
await assert.rejects(
|
|
297
401
|
() => createWorktrees(repoDir, "extension-subdir", 1, { baseDir: path.join(extensionsDir, "checkout") }),
|
|
298
402
|
/worktree base directory cannot be inside Pi extensions directory/i,
|
|
299
403
|
);
|
|
@@ -309,7 +413,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
309
413
|
}
|
|
310
414
|
});
|
|
311
415
|
|
|
312
|
-
it("rejects symlinked worktree base directories inside Pi extensions", () => {
|
|
416
|
+
it("rejects symlinked worktree base directories inside Pi extensions", async () => {
|
|
313
417
|
const repoDir = createRepo("pi-worktree-extension-symlink-");
|
|
314
418
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-home-"));
|
|
315
419
|
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
@@ -323,7 +427,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
323
427
|
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
324
428
|
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
325
429
|
fs.symlinkSync(extensionsDir, aliasDir, process.platform === "win32" ? "junction" : "dir");
|
|
326
|
-
assert.
|
|
430
|
+
await assert.rejects(
|
|
327
431
|
() => createWorktrees(repoDir, "extension-symlink", 1, { baseDir: path.join(aliasDir, "checkout") }),
|
|
328
432
|
/worktree base directory cannot be inside Pi extensions directory/i,
|
|
329
433
|
);
|
|
@@ -339,15 +443,96 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
339
443
|
}
|
|
340
444
|
});
|
|
341
445
|
|
|
342
|
-
it("
|
|
446
|
+
it("rejects a final project directory inside Pi extensions", async () => {
|
|
447
|
+
const sourceRepo = createRepo("pi-worktree-extension-project-");
|
|
448
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-home-"));
|
|
449
|
+
const repoDir = path.join(tempHome, "extensions");
|
|
450
|
+
const agentDir = path.join(tempHome, ".selesai", "agent");
|
|
451
|
+
const extensionsDir = path.join(agentDir, "extensions");
|
|
452
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
453
|
+
const previousHome = process.env.HOME;
|
|
454
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
455
|
+
try {
|
|
456
|
+
fs.renameSync(sourceRepo, repoDir);
|
|
457
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
458
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
459
|
+
process.env.HOME = tempHome;
|
|
460
|
+
process.env.USERPROFILE = tempHome;
|
|
461
|
+
|
|
462
|
+
await assert.rejects(
|
|
463
|
+
() => createWorktrees(repoDir, "extension-project", 1, { provider: "native", baseDir: agentDir }),
|
|
464
|
+
/worktree path cannot be inside Pi extensions directory/i,
|
|
465
|
+
);
|
|
466
|
+
assert.throws(
|
|
467
|
+
() => resolveExpectedWorktreeAgentCwd(repoDir, "extension-project", 0, agentDir),
|
|
468
|
+
/worktree path cannot be inside Pi extensions directory/i,
|
|
469
|
+
);
|
|
470
|
+
assert.deepEqual(git(repoDir, ["worktree", "list", "--porcelain"]).split("\n").filter((line) => line.startsWith("worktree ")), [`worktree ${git(repoDir, ["rev-parse", "--show-toplevel"])}`]);
|
|
471
|
+
assert.deepEqual(fs.readdirSync(extensionsDir), []);
|
|
472
|
+
} finally {
|
|
473
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
474
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
475
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
476
|
+
else process.env.HOME = previousHome;
|
|
477
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
478
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
479
|
+
cleanupRepo(repoDir);
|
|
480
|
+
cleanupRepo(sourceRepo);
|
|
481
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("rejects a project directory symlink into Pi extensions", async () => {
|
|
486
|
+
const repoDir = createRepo("pi-worktree-extension-project-symlink-");
|
|
487
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-home-"));
|
|
488
|
+
const agentDir = path.join(tempHome, ".selesai", "agent");
|
|
489
|
+
const extensionsDir = path.join(agentDir, "extensions");
|
|
490
|
+
const baseDir = path.join(tempHome, "worktree-root");
|
|
491
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
492
|
+
const previousHome = process.env.HOME;
|
|
493
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
494
|
+
try {
|
|
495
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
496
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
497
|
+
fs.symlinkSync(extensionsDir, path.join(baseDir, path.basename(repoDir)), process.platform === "win32" ? "junction" : "dir");
|
|
498
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
499
|
+
process.env.HOME = tempHome;
|
|
500
|
+
process.env.USERPROFILE = tempHome;
|
|
501
|
+
|
|
502
|
+
await assert.rejects(
|
|
503
|
+
() => createWorktrees(repoDir, "extension-project-symlink", 1, { provider: "native", baseDir }),
|
|
504
|
+
/worktree path cannot be inside Pi extensions directory/i,
|
|
505
|
+
);
|
|
506
|
+
assert.throws(
|
|
507
|
+
() => resolveExpectedWorktreeAgentCwd(repoDir, "extension-project-symlink", 0, baseDir),
|
|
508
|
+
/worktree path cannot be inside Pi extensions directory/i,
|
|
509
|
+
);
|
|
510
|
+
assert.deepEqual(git(repoDir, ["worktree", "list", "--porcelain"]).split("\n").filter((line) => line.startsWith("worktree ")), [`worktree ${git(repoDir, ["rev-parse", "--show-toplevel"])}`]);
|
|
511
|
+
assert.deepEqual(fs.readdirSync(extensionsDir), []);
|
|
512
|
+
} finally {
|
|
513
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
514
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
515
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
516
|
+
else process.env.HOME = previousHome;
|
|
517
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
518
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
519
|
+
cleanupRepo(repoDir, baseDir);
|
|
520
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("uses SELESAI_SUBAGENTS_WORKTREE_DIR when no base directory is configured", async () => {
|
|
343
525
|
const repoDir = createRepo("pi-worktree-env-base-dir-");
|
|
344
526
|
const previous = process.env.SELESAI_SUBAGENTS_WORKTREE_DIR;
|
|
345
527
|
const baseDir = path.join(os.tmpdir(), `pi-worktree-env-base-${Date.now().toString(36)}`);
|
|
346
528
|
let setup: WorktreeSetup | undefined;
|
|
347
529
|
try {
|
|
348
530
|
process.env.SELESAI_SUBAGENTS_WORKTREE_DIR = baseDir;
|
|
349
|
-
setup = createWorktrees(repoDir, "env-base-dir", 1);
|
|
350
|
-
assert.equal(
|
|
531
|
+
setup = await createWorktrees(repoDir, "env-base-dir", 1);
|
|
532
|
+
assert.equal(
|
|
533
|
+
setup.worktrees[0]!.path,
|
|
534
|
+
path.join(baseDir, path.basename(repoDir), "pi-worktree-env-base-dir-0"),
|
|
535
|
+
);
|
|
351
536
|
} finally {
|
|
352
537
|
if (setup) cleanupWorktrees(setup);
|
|
353
538
|
if (previous === undefined) {
|
|
@@ -355,17 +540,104 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
355
540
|
} else {
|
|
356
541
|
process.env.SELESAI_SUBAGENTS_WORKTREE_DIR = previous;
|
|
357
542
|
}
|
|
543
|
+
cleanupRepo(repoDir, baseDir);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("rejects empty worktree base directory", async () => {
|
|
548
|
+
const repoDir = createRepo("pi-worktree-empty-base-");
|
|
549
|
+
try {
|
|
550
|
+
await assert.rejects(
|
|
551
|
+
() => createWorktrees(repoDir, "empty-base", 1, { baseDir: " " }),
|
|
552
|
+
/worktree base directory cannot be empty/,
|
|
553
|
+
);
|
|
554
|
+
} finally {
|
|
555
|
+
cleanupRepo(repoDir);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("rejects worktree paths that would land inside the repository checkout", async () => {
|
|
560
|
+
const repoDir = createRepo("pi-worktree-inside-");
|
|
561
|
+
const repoParent = path.dirname(repoDir);
|
|
562
|
+
const leakedLeaf = "pi-worktree-inside-0";
|
|
563
|
+
try {
|
|
564
|
+
await assert.rejects(
|
|
565
|
+
() => createWorktrees(repoDir, "inside", 1, { baseDir: repoParent }),
|
|
566
|
+
/inside the repository/i,
|
|
567
|
+
);
|
|
568
|
+
assert.throws(
|
|
569
|
+
() => resolveExpectedWorktreeAgentCwd(repoDir, "inside", 0, repoParent),
|
|
570
|
+
/inside the repository/i,
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const porcelain = git(repoDir, ["worktree", "list", "--porcelain"]);
|
|
574
|
+
const worktreeLines = porcelain.split("\n").filter((line) => line.startsWith("worktree "));
|
|
575
|
+
assert.deepEqual(worktreeLines, [`worktree ${git(repoDir, ["rev-parse", "--show-toplevel"])}`]);
|
|
576
|
+
assert.equal(fs.existsSync(path.join(repoDir, leakedLeaf)), false);
|
|
577
|
+
assert.equal(fs.existsSync(path.join(repoParent, leakedLeaf)), false);
|
|
578
|
+
} finally {
|
|
579
|
+
cleanupRepo(repoDir);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("rejects worktree paths that are direct children of the repository parent", async () => {
|
|
584
|
+
const repoDir = createRepo("pi-worktree-parent-child-");
|
|
585
|
+
const repoParent = path.dirname(repoDir);
|
|
586
|
+
const dedicatedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-parent-child-root-"));
|
|
587
|
+
const projectDir = path.join(dedicatedRoot, path.basename(repoDir));
|
|
588
|
+
const leakedLeaf = path.join(repoParent, "pi-worktree-parent-child-0");
|
|
589
|
+
try {
|
|
590
|
+
fs.symlinkSync(repoParent, projectDir, process.platform === "win32" ? "junction" : "dir");
|
|
591
|
+
await assert.rejects(
|
|
592
|
+
() => createWorktrees(repoDir, "parent-child", 1, { baseDir: dedicatedRoot }),
|
|
593
|
+
/direct child of the repository parent/i,
|
|
594
|
+
);
|
|
595
|
+
assert.throws(
|
|
596
|
+
() => resolveExpectedWorktreeAgentCwd(repoDir, "parent-child", 0, dedicatedRoot),
|
|
597
|
+
/direct child of the repository parent/i,
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const porcelain = git(repoDir, ["worktree", "list", "--porcelain"]);
|
|
601
|
+
const worktreeLines = porcelain.split("\n").filter((line) => line.startsWith("worktree "));
|
|
602
|
+
assert.deepEqual(worktreeLines, [`worktree ${git(repoDir, ["rev-parse", "--show-toplevel"])}`]);
|
|
603
|
+
assert.equal(fs.existsSync(leakedLeaf), false);
|
|
604
|
+
} finally {
|
|
605
|
+
try { fs.rmSync(leakedLeaf, { recursive: true, force: true }); } catch {}
|
|
606
|
+
fs.rmSync(dedicatedRoot, { recursive: true, force: true });
|
|
358
607
|
cleanupRepo(repoDir);
|
|
359
|
-
fs.rmSync(baseDir, { recursive: true, force: true });
|
|
360
608
|
}
|
|
361
609
|
});
|
|
362
610
|
|
|
363
|
-
it("
|
|
611
|
+
it("does not mkdir inside the checkout when rejecting unsafe locations", async () => {
|
|
612
|
+
const repoDir = createRepo("pi-worktree-no-mkdir-inside-");
|
|
613
|
+
try {
|
|
614
|
+
await assert.rejects(
|
|
615
|
+
() => createWorktrees(repoDir, "inside-self", 1, { baseDir: repoDir }),
|
|
616
|
+
/inside the repository/i,
|
|
617
|
+
);
|
|
618
|
+
assert.throws(
|
|
619
|
+
() => resolveExpectedWorktreeAgentCwd(repoDir, "inside-self", 0, repoDir),
|
|
620
|
+
/inside the repository/i,
|
|
621
|
+
);
|
|
622
|
+
assert.equal(fs.existsSync(path.join(repoDir, path.basename(repoDir))), false);
|
|
623
|
+
|
|
624
|
+
await assert.rejects(
|
|
625
|
+
() => createWorktrees(repoDir, "rel-worktrees", 1, { baseDir: "worktrees" }),
|
|
626
|
+
/inside the repository/i,
|
|
627
|
+
);
|
|
628
|
+
assert.equal(fs.existsSync(path.join(repoDir, "worktrees")), false);
|
|
629
|
+
assert.equal(fs.existsSync(path.join(repoDir, "worktrees", path.basename(repoDir))), false);
|
|
630
|
+
} finally {
|
|
631
|
+
cleanupRepo(repoDir);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("createWorktrees rejects dirty repositories before resolving the requested base ref", async () => {
|
|
364
636
|
const repoDir = createRepo("pi-worktree-dirty-");
|
|
365
637
|
try {
|
|
366
638
|
fs.writeFileSync(path.join(repoDir, "tracked.txt"), "dirty\n", "utf-8");
|
|
367
|
-
assert.
|
|
368
|
-
() => createWorktrees(repoDir, "dirty", 1),
|
|
639
|
+
await assert.rejects(
|
|
640
|
+
() => createWorktrees(repoDir, "dirty", 1, { baseRef: "unsafe..ref" }),
|
|
369
641
|
/worktree isolation requires a clean git working tree/i,
|
|
370
642
|
);
|
|
371
643
|
} finally {
|
|
@@ -373,13 +645,64 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
373
645
|
}
|
|
374
646
|
});
|
|
375
647
|
|
|
376
|
-
it("
|
|
648
|
+
it("allocates from baseRef while preserving source HEAD and propagating baseCommit to hooks and diffs", { skip: hookScriptSkip }, async () => {
|
|
649
|
+
const repoDir = createRepo("pi-worktree-base-ref-");
|
|
650
|
+
const firstCommit = git(repoDir, ["rev-parse", "HEAD"]);
|
|
651
|
+
git(repoDir, ["branch", "release"]);
|
|
652
|
+
fs.writeFileSync(path.join(repoDir, "tracked.txt"), "second\n", "utf-8");
|
|
653
|
+
git(repoDir, ["add", "tracked.txt"]);
|
|
654
|
+
git(repoDir, ["commit", "-m", "second commit"]);
|
|
655
|
+
const sourceHead = git(repoDir, ["rev-parse", "HEAD"]);
|
|
656
|
+
const hookPath = createHookScript(repoDir, "base-ref-hook.mjs", `
|
|
657
|
+
import * as fs from "node:fs";
|
|
658
|
+
const payload = JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
659
|
+
fs.writeFileSync(payload.worktreePath + "/.base-commit", payload.baseCommit + "\\n", "utf-8");
|
|
660
|
+
process.stdout.write(JSON.stringify({ syntheticPaths: [".base-commit"] }));
|
|
661
|
+
`);
|
|
662
|
+
let setup: WorktreeSetup | undefined;
|
|
663
|
+
try {
|
|
664
|
+
setup = await createWorktrees(repoDir, "base-ref", 1, {
|
|
665
|
+
provider: "native",
|
|
666
|
+
baseRef: "release",
|
|
667
|
+
setupHook: { hookPath: path.relative(repoDir, hookPath) },
|
|
668
|
+
});
|
|
669
|
+
const worktree = setup.worktrees[0]!;
|
|
670
|
+
assert.equal(setup.baseCommit, firstCommit);
|
|
671
|
+
assert.equal(git(worktree.path, ["rev-parse", "HEAD"]), firstCommit);
|
|
672
|
+
assert.equal(git(repoDir, ["rev-parse", "HEAD"]), sourceHead);
|
|
673
|
+
assert.equal(fs.readFileSync(path.join(worktree.path, ".base-commit"), "utf-8").trim(), firstCommit);
|
|
674
|
+
fs.writeFileSync(path.join(worktree.path, "tracked.txt"), "agent change\n", "utf-8");
|
|
675
|
+
const diffs = diffWorktrees(setup, ["worker"], path.join(repoDir, "artifacts", "base-ref"));
|
|
676
|
+
assert.equal(diffs[0]?.error, undefined);
|
|
677
|
+
assert.match(fs.readFileSync(diffs[0]!.patchPath, "utf-8"), /tracked\.txt/);
|
|
678
|
+
} finally {
|
|
679
|
+
if (setup) cleanupWorktrees(setup, { kind: "setup-rollback" });
|
|
680
|
+
cleanupRepo(repoDir);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("rejects unsafe and unresolved base refs before allocation", async () => {
|
|
685
|
+
const repoDir = createRepo("pi-worktree-invalid-base-ref-");
|
|
686
|
+
try {
|
|
687
|
+
for (const baseRef of ["unsafe..ref", "branch name", "HEAD^{tree}", "@", "a".repeat(40), "a".repeat(64)] as const) {
|
|
688
|
+
await assert.rejects(() => createWorktrees(repoDir, `invalid-${baseRef.length}`, 1, { provider: "native", baseRef }), /baseRef.*HEAD.*named ref.*40\/64-character commit IDs.*revision expressions.*unsupported/);
|
|
689
|
+
}
|
|
690
|
+
git(repoDir, ["tag", "tree-object", "HEAD^{tree}"]);
|
|
691
|
+
await assert.rejects(() => createWorktrees(repoDir, "invalid-tree", 1, { provider: "native", baseRef: "tree-object" }), /could not be resolved to a commit/i);
|
|
692
|
+
await assert.rejects(() => createWorktrees(repoDir, "invalid-missing", 1, { provider: "native", baseRef: "refs/heads/missing" }), /could not be resolved to a commit/i);
|
|
693
|
+
assert.equal(git(repoDir, ["worktree", "list", "--porcelain"]).split("\n").filter((line) => line.startsWith("worktree ")).length, 1);
|
|
694
|
+
} finally {
|
|
695
|
+
cleanupRepo(repoDir);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("createWorktrees ignores pi-subagents runtime artifacts in the source checkout", async () => {
|
|
377
700
|
const repoDir = createRepo("pi-worktree-runtime-artifacts-");
|
|
378
701
|
let setup: WorktreeSetup | undefined;
|
|
379
702
|
try {
|
|
380
703
|
fs.mkdirSync(path.join(repoDir, ".pi-subagents", "missions"), { recursive: true });
|
|
381
704
|
fs.writeFileSync(path.join(repoDir, ".pi-subagents", "missions", "mission.json"), "{}\n", "utf-8");
|
|
382
|
-
setup = createWorktrees(repoDir, "runtime-artifacts", 1);
|
|
705
|
+
setup = await createWorktrees(repoDir, "runtime-artifacts", 1);
|
|
383
706
|
assert.equal(setup.worktrees.length, 1);
|
|
384
707
|
} finally {
|
|
385
708
|
if (setup) cleanupWorktrees(setup);
|
|
@@ -428,7 +751,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
428
751
|
});
|
|
429
752
|
});
|
|
430
753
|
|
|
431
|
-
it("diffWorktrees captures committed, modified, and new files without staging the node_modules symlink", () => {
|
|
754
|
+
it("diffWorktrees captures committed, modified, and new files without staging the node_modules symlink", async () => {
|
|
432
755
|
const repoDir = createRepo("pi-worktree-diff-");
|
|
433
756
|
const nodeModulesDir = path.join(repoDir, "node_modules");
|
|
434
757
|
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
@@ -436,7 +759,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
436
759
|
|
|
437
760
|
let setup: WorktreeSetup | undefined;
|
|
438
761
|
try {
|
|
439
|
-
setup = createWorktrees(repoDir, `diff-${process.pid}`, 1);
|
|
762
|
+
setup = await createWorktrees(repoDir, `diff-${process.pid}`, 1);
|
|
440
763
|
const worktree = setup.worktrees[0]!;
|
|
441
764
|
fs.writeFileSync(path.join(worktree.path, "committed.ts"), "export const committed = true;\n", "utf-8");
|
|
442
765
|
git(worktree.path, ["add", "committed.ts"]);
|
|
@@ -467,11 +790,58 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
467
790
|
}
|
|
468
791
|
});
|
|
469
792
|
|
|
470
|
-
it("
|
|
793
|
+
it("captures applyable patches despite external diffs and display configuration", { skip: hookScriptSkip }, async () => {
|
|
794
|
+
const repoDir = createRepo("pi-worktree-diff-external-");
|
|
795
|
+
const externalDiffPath = createHookScript(repoDir, "external-diff.mjs", `
|
|
796
|
+
process.stdout.write("corrupt external diff output\\n");
|
|
797
|
+
`);
|
|
798
|
+
let setup: WorktreeSetup | undefined;
|
|
799
|
+
try {
|
|
800
|
+
git(repoDir, ["config", "diff.external", externalDiffPath]);
|
|
801
|
+
git(repoDir, ["config", "color.ui", "always"]);
|
|
802
|
+
git(repoDir, ["config", "diff.noprefix", "true"]);
|
|
803
|
+
git(repoDir, ["config", "diff.linePrefix", "corrupt display prefix "]);
|
|
804
|
+
git(repoDir, ["config", "diff.relative", "true"]);
|
|
805
|
+
setup = await createWorktrees(repoDir, "diff-external", 1);
|
|
806
|
+
const worktree = setup.worktrees[0]!;
|
|
807
|
+
fs.writeFileSync(path.join(worktree.path, "tracked.txt"), "external-safe\n", "utf-8");
|
|
808
|
+
|
|
809
|
+
const diffs = diffWorktrees(setup, ["worker"], path.join(repoDir, "artifacts", "external"));
|
|
810
|
+
assert.equal(diffs[0]?.error, undefined);
|
|
811
|
+
const patch = fs.readFileSync(diffs[0]!.patchPath, "utf-8");
|
|
812
|
+
assert.match(patch, /^diff --git a\/tracked\.txt b\/tracked\.txt/m);
|
|
813
|
+
assert.doesNotMatch(patch, /corrupt external diff output|corrupt display prefix|\u001b/);
|
|
814
|
+
assert.equal(git(worktree.path, ["apply", "--check", "--cached", "--reverse", diffs[0]!.patchPath]), "");
|
|
815
|
+
} finally {
|
|
816
|
+
if (setup) cleanupWorktrees(setup, { kind: "setup-rollback" });
|
|
817
|
+
cleanupRepo(repoDir);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("includes binary data in captured patches", async () => {
|
|
822
|
+
const repoDir = createRepo("pi-worktree-diff-binary-");
|
|
823
|
+
let setup: WorktreeSetup | undefined;
|
|
824
|
+
try {
|
|
825
|
+
setup = await createWorktrees(repoDir, "diff-binary", 1);
|
|
826
|
+
const worktree = setup.worktrees[0]!;
|
|
827
|
+
fs.writeFileSync(path.join(worktree.path, "binary.dat"), Buffer.from([0, 1, 2, 255, 0, 3]));
|
|
828
|
+
|
|
829
|
+
const diffs = diffWorktrees(setup, ["worker"], path.join(repoDir, "artifacts", "binary"));
|
|
830
|
+
assert.equal(diffs[0]?.error, undefined);
|
|
831
|
+
const patch = fs.readFileSync(diffs[0]!.patchPath, "utf-8");
|
|
832
|
+
assert.match(patch, /GIT binary patch/);
|
|
833
|
+
assert.equal(git(worktree.path, ["apply", "--check", "--cached", "--reverse", diffs[0]!.patchPath]), "");
|
|
834
|
+
} finally {
|
|
835
|
+
if (setup) cleanupWorktrees(setup, { kind: "setup-rollback" });
|
|
836
|
+
cleanupRepo(repoDir);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("cleanupWorktrees removes worktrees and branches", async () => {
|
|
471
841
|
const repoDir = createRepo("pi-worktree-cleanup-");
|
|
472
842
|
let setup: WorktreeSetup | undefined;
|
|
473
843
|
try {
|
|
474
|
-
setup = createWorktrees(repoDir, "cleanup", 2);
|
|
844
|
+
setup = await createWorktrees(repoDir, "cleanup", 2);
|
|
475
845
|
const worktreePaths = setup.worktrees.map((worktree) => worktree.path);
|
|
476
846
|
const branches = setup.worktrees.map((worktree) => worktree.branch);
|
|
477
847
|
const cleanup = cleanupWorktrees(setup);
|
|
@@ -494,11 +864,11 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
494
864
|
}
|
|
495
865
|
});
|
|
496
866
|
|
|
497
|
-
it("preserves dirty worktrees when no patch was captured", () => {
|
|
867
|
+
it("preserves dirty worktrees when no patch was captured", async () => {
|
|
498
868
|
const repoDir = createRepo("pi-worktree-uncaptured-");
|
|
499
869
|
let setup: WorktreeSetup | undefined;
|
|
500
870
|
try {
|
|
501
|
-
setup = createWorktrees(repoDir, "uncaptured", 1);
|
|
871
|
+
setup = await createWorktrees(repoDir, "uncaptured", 1);
|
|
502
872
|
const worktree = setup.worktrees[0]!;
|
|
503
873
|
fs.writeFileSync(path.join(worktree.path, "tracked.txt"), "uncaptured\n", "utf-8");
|
|
504
874
|
|
|
@@ -521,11 +891,11 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
521
891
|
}
|
|
522
892
|
});
|
|
523
893
|
|
|
524
|
-
it("preserves committed branch work when no patch was captured", () => {
|
|
894
|
+
it("preserves committed branch work when no patch was captured", async () => {
|
|
525
895
|
const repoDir = createRepo("pi-worktree-committed-uncaptured-");
|
|
526
896
|
let setup: WorktreeSetup | undefined;
|
|
527
897
|
try {
|
|
528
|
-
setup = createWorktrees(repoDir, "committed-uncaptured", 1);
|
|
898
|
+
setup = await createWorktrees(repoDir, "committed-uncaptured", 1);
|
|
529
899
|
const worktree = setup.worktrees[0]!;
|
|
530
900
|
fs.writeFileSync(path.join(worktree.path, "committed.txt"), "unlanded commit\n", "utf-8");
|
|
531
901
|
git(worktree.path, ["add", "committed.txt"]);
|
|
@@ -545,11 +915,36 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
545
915
|
}
|
|
546
916
|
});
|
|
547
917
|
|
|
548
|
-
it("
|
|
918
|
+
it("does not let trusted external diff hide uncaptured committed work during cleanup", { skip: hookScriptSkip }, async () => {
|
|
919
|
+
const repoDir = createRepo("pi-worktree-cleanup-external-diff-");
|
|
920
|
+
const externalDiffPath = createHookScript(repoDir, "external-diff-clean.mjs", "process.exit(0);");
|
|
921
|
+
let setup: WorktreeSetup | undefined;
|
|
922
|
+
try {
|
|
923
|
+
git(repoDir, ["config", "diff.external", externalDiffPath]);
|
|
924
|
+
git(repoDir, ["config", "diff.trustExitCode", "true"]);
|
|
925
|
+
setup = await createWorktrees(repoDir, "cleanup-external-diff", 1);
|
|
926
|
+
const worktree = setup.worktrees[0]!;
|
|
927
|
+
fs.writeFileSync(path.join(worktree.path, "committed.txt"), "unlanded work\n", "utf-8");
|
|
928
|
+
git(worktree.path, ["add", "committed.txt"]);
|
|
929
|
+
git(worktree.path, ["commit", "-m", "unlanded work"]);
|
|
930
|
+
|
|
931
|
+
const cleanup = cleanupWorktrees(setup);
|
|
932
|
+
assert.equal(cleanup.state, "partial");
|
|
933
|
+
assert.equal(cleanup.tasks[0]?.preserved, true);
|
|
934
|
+
assert.match(cleanup.tasks[0]?.reason ?? "", /not represented by a captured handoff patch/);
|
|
935
|
+
assert.equal(fs.existsSync(worktree.path), true);
|
|
936
|
+
assert.notEqual(git(repoDir, ["branch", "--list", worktree.branch]), "");
|
|
937
|
+
} finally {
|
|
938
|
+
if (setup) cleanupWorktrees(setup, { kind: "setup-rollback" });
|
|
939
|
+
cleanupRepo(repoDir);
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it("removes dirty worktrees only after their captured patch is recorded in the handoff manifest", async () => {
|
|
549
944
|
const repoDir = createRepo("pi-worktree-captured-");
|
|
550
945
|
let setup: WorktreeSetup | undefined;
|
|
551
946
|
try {
|
|
552
|
-
setup = createWorktrees(repoDir, "captured", 1);
|
|
947
|
+
setup = await createWorktrees(repoDir, "captured", 1);
|
|
553
948
|
const worktreePath = setup.worktrees[0]!.path;
|
|
554
949
|
fs.writeFileSync(path.join(worktreePath, "tracked.txt"), "captured\n", "utf-8");
|
|
555
950
|
const diffs = diffWorktrees(setup, ["worker"], path.join(repoDir, "artifacts", "captured"));
|
|
@@ -565,7 +960,21 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
565
960
|
version: 1,
|
|
566
961
|
groups: [{ children: [{ patch: { path: diffs[0]!.patchPath } }] }],
|
|
567
962
|
}), "utf-8");
|
|
568
|
-
|
|
963
|
+
fs.writeFileSync(diffs[0]!.patchPath, "corrupt patch\n", "utf-8");
|
|
964
|
+
const invalidPatch = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath });
|
|
965
|
+
assert.equal(invalidPatch.state, "partial");
|
|
966
|
+
assert.match(invalidPatch.tasks[0]?.reason ?? "", /failed validation/);
|
|
967
|
+
assert.equal(fs.existsSync(worktreePath), true);
|
|
968
|
+
|
|
969
|
+
const validatedDiffs = diffWorktrees(setup, ["worker"], path.join(repoDir, "artifacts", "captured"));
|
|
970
|
+
fs.writeFileSync(path.join(worktreePath, "late.txt"), "late\n", "utf-8");
|
|
971
|
+
const stalePatch = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: validatedDiffs, handoffManifestPath });
|
|
972
|
+
assert.equal(stalePatch.state, "partial");
|
|
973
|
+
assert.match(stalePatch.tasks[0]?.reason ?? "", /does not match current worktree changes/);
|
|
974
|
+
assert.equal(fs.existsSync(worktreePath), true);
|
|
975
|
+
|
|
976
|
+
const currentDiffs = diffWorktrees(setup, ["worker"], path.join(repoDir, "artifacts", "captured"));
|
|
977
|
+
const cleanup = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: currentDiffs, handoffManifestPath });
|
|
569
978
|
assert.equal(cleanup.state, "complete");
|
|
570
979
|
assert.equal(fs.existsSync(worktreePath), false);
|
|
571
980
|
setup = undefined;
|
|
@@ -577,7 +986,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
577
986
|
|
|
578
987
|
it("createWorktrees creates node_modules symlink when node_modules exists", {
|
|
579
988
|
skip: process.platform === "win32" ? "Symlink behavior differs on Windows CI environments." : undefined,
|
|
580
|
-
}, () => {
|
|
989
|
+
}, async () => {
|
|
581
990
|
const repoDir = createRepo("pi-worktree-node-modules-");
|
|
582
991
|
const nodeModulesDir = path.join(repoDir, "node_modules");
|
|
583
992
|
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
@@ -585,7 +994,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
585
994
|
|
|
586
995
|
let setup: WorktreeSetup | undefined;
|
|
587
996
|
try {
|
|
588
|
-
setup = createWorktrees(repoDir, "node-modules", 1);
|
|
997
|
+
setup = await createWorktrees(repoDir, "node-modules", 1);
|
|
589
998
|
const symlinkPath = path.join(setup.worktrees[0]!.path, "node_modules");
|
|
590
999
|
assert.equal(setup.worktrees[0]!.nodeModulesLinked, true);
|
|
591
1000
|
assert.deepEqual(setup.worktrees[0]!.syntheticPaths, ["node_modules"]);
|
|
@@ -600,7 +1009,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
600
1009
|
|
|
601
1010
|
it("diffWorktrees preserves a tracked node_modules symlink", {
|
|
602
1011
|
skip: process.platform === "win32" ? "Symlink behavior differs on Windows CI environments." : undefined,
|
|
603
|
-
}, () => {
|
|
1012
|
+
}, async () => {
|
|
604
1013
|
const repoDir = createRepo("pi-worktree-tracked-node-modules-");
|
|
605
1014
|
const vendorDir = path.join(repoDir, "vendor-modules");
|
|
606
1015
|
fs.mkdirSync(vendorDir, { recursive: true });
|
|
@@ -611,7 +1020,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
611
1020
|
|
|
612
1021
|
let setup: WorktreeSetup | undefined;
|
|
613
1022
|
try {
|
|
614
|
-
setup = createWorktrees(repoDir, `tracked-node-modules-${process.pid}`, 1);
|
|
1023
|
+
setup = await createWorktrees(repoDir, `tracked-node-modules-${process.pid}`, 1);
|
|
615
1024
|
assert.equal(setup.worktrees[0]!.nodeModulesLinked, false);
|
|
616
1025
|
assert.deepEqual(setup.worktrees[0]!.syntheticPaths, []);
|
|
617
1026
|
fs.writeFileSync(path.join(setup.worktrees[0]!.path, "tracked.txt"), "modified\n", "utf-8");
|
|
@@ -627,7 +1036,7 @@ console.log(JSON.stringify({ action: "created", branch, path: repo, created_bran
|
|
|
627
1036
|
}
|
|
628
1037
|
});
|
|
629
1038
|
|
|
630
|
-
it("runs a repo-relative worktree setup hook and records synthetic paths", { skip: hookScriptSkip }, () => {
|
|
1039
|
+
it("runs a repo-relative worktree setup hook and records synthetic paths", { skip: hookScriptSkip }, async () => {
|
|
631
1040
|
const repoDir = createRepo("pi-worktree-hook-relative-");
|
|
632
1041
|
const hookPath = createHookScript(repoDir, "setup-hook.mjs", `
|
|
633
1042
|
import * as fs from "node:fs";
|
|
@@ -640,7 +1049,7 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [".venv"] }));
|
|
|
640
1049
|
|
|
641
1050
|
let setup: WorktreeSetup | undefined;
|
|
642
1051
|
try {
|
|
643
|
-
setup = createWorktrees(repoDir, "hook-relative", 1, {
|
|
1052
|
+
setup = await createWorktrees(repoDir, "hook-relative", 1, {
|
|
644
1053
|
setupHook: { hookPath: path.relative(repoDir, hookPath) },
|
|
645
1054
|
});
|
|
646
1055
|
assert.ok(setup.worktrees[0]!.syntheticPaths.includes(".venv"));
|
|
@@ -650,7 +1059,7 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [".venv"] }));
|
|
|
650
1059
|
}
|
|
651
1060
|
});
|
|
652
1061
|
|
|
653
|
-
it("runs an absolute worktree setup hook path", { skip: hookScriptSkip }, () => {
|
|
1062
|
+
it("runs an absolute worktree setup hook path", { skip: hookScriptSkip }, async () => {
|
|
654
1063
|
const repoDir = createRepo("pi-worktree-hook-absolute-");
|
|
655
1064
|
const hookPath = createHookScript(repoDir, "setup-hook.mjs", `
|
|
656
1065
|
import * as fs from "node:fs";
|
|
@@ -660,7 +1069,7 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
|
|
|
660
1069
|
|
|
661
1070
|
let setup: WorktreeSetup | undefined;
|
|
662
1071
|
try {
|
|
663
|
-
setup = createWorktrees(repoDir, "hook-absolute", 1, {
|
|
1072
|
+
setup = await createWorktrees(repoDir, "hook-absolute", 1, {
|
|
664
1073
|
setupHook: { hookPath },
|
|
665
1074
|
});
|
|
666
1075
|
assert.equal(setup.worktrees.length, 1);
|
|
@@ -670,10 +1079,10 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
|
|
|
670
1079
|
}
|
|
671
1080
|
});
|
|
672
1081
|
|
|
673
|
-
it("rejects bare command names for worktree setup hooks", () => {
|
|
1082
|
+
it("rejects bare command names for worktree setup hooks", async () => {
|
|
674
1083
|
const repoDir = createRepo("pi-worktree-hook-bare-");
|
|
675
1084
|
try {
|
|
676
|
-
assert.
|
|
1085
|
+
await assert.rejects(
|
|
677
1086
|
() => createWorktrees(repoDir, "hook-bare", 1, { setupHook: { hookPath: "node" } }),
|
|
678
1087
|
/worktree setup hook must be an absolute path or a repo-relative path/i,
|
|
679
1088
|
);
|
|
@@ -682,7 +1091,8 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
|
|
|
682
1091
|
}
|
|
683
1092
|
});
|
|
684
1093
|
|
|
685
|
-
it("rejects tracked synthetic paths from hook output", { skip: hookScriptSkip }, () => {
|
|
1094
|
+
it("rejects tracked synthetic paths from hook output", { skip: hookScriptSkip }, async () => {
|
|
1095
|
+
if (await runPoisonCaseInChild("rejects tracked synthetic paths from hook output")) return;
|
|
686
1096
|
const repoDir = createRepo("pi-worktree-hook-tracked-");
|
|
687
1097
|
const hookPath = createHookScript(repoDir, "tracked-hook.mjs", `
|
|
688
1098
|
import * as fs from "node:fs";
|
|
@@ -690,17 +1100,24 @@ JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
|
690
1100
|
process.stdout.write(JSON.stringify({ syntheticPaths: ["tracked.txt"] }));
|
|
691
1101
|
`);
|
|
692
1102
|
const runId = `hook-tracked-${Date.now().toString(36)}`;
|
|
1103
|
+
let retained: WorktreeSetup | undefined;
|
|
693
1104
|
try {
|
|
694
|
-
assert.
|
|
1105
|
+
await assert.rejects(
|
|
695
1106
|
() => createWorktrees(repoDir, runId, 1, { setupHook: { hookPath: path.relative(repoDir, hookPath) } }),
|
|
696
|
-
|
|
1107
|
+
(error: unknown) => {
|
|
1108
|
+
assert.ok(error instanceof WorktreeSetupError);
|
|
1109
|
+
retained = error.snapshot.setup;
|
|
1110
|
+
return assertRetainedHookFailure(error, /cannot mark tracked paths as synthetic/i);
|
|
1111
|
+
},
|
|
697
1112
|
);
|
|
698
1113
|
} finally {
|
|
1114
|
+
for (const worktree of retained?.worktrees ?? []) fs.rmSync(worktree.path, { recursive: true, force: true });
|
|
699
1115
|
cleanupRepo(repoDir);
|
|
700
1116
|
}
|
|
701
1117
|
});
|
|
702
1118
|
|
|
703
|
-
it("rejects absolute synthetic paths from hook output", { skip: hookScriptSkip }, () => {
|
|
1119
|
+
it("rejects absolute synthetic paths from hook output", { skip: hookScriptSkip }, async () => {
|
|
1120
|
+
if (await runPoisonCaseInChild("rejects absolute synthetic paths from hook output")) return;
|
|
704
1121
|
const repoDir = createRepo("pi-worktree-hook-absolute-synthetic-");
|
|
705
1122
|
const hookPath = createHookScript(repoDir, "absolute-path-hook.mjs", `
|
|
706
1123
|
import * as fs from "node:fs";
|
|
@@ -708,17 +1125,23 @@ const payload = JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
|
708
1125
|
process.stdout.write(JSON.stringify({ syntheticPaths: [payload.worktreePath + "/.venv"] }));
|
|
709
1126
|
`);
|
|
710
1127
|
const runId = `hook-absolute-synthetic-${Date.now().toString(36)}`;
|
|
1128
|
+
let retained: WorktreeSetup | undefined;
|
|
711
1129
|
try {
|
|
712
|
-
assert.
|
|
1130
|
+
await assert.rejects(
|
|
713
1131
|
() => createWorktrees(repoDir, runId, 1, { setupHook: { hookPath: path.relative(repoDir, hookPath) } }),
|
|
714
|
-
|
|
1132
|
+
(error: unknown) => {
|
|
1133
|
+
assert.ok(error instanceof WorktreeSetupError);
|
|
1134
|
+
retained = error.snapshot.setup;
|
|
1135
|
+
return assertRetainedHookFailure(error, /synthetic path must be relative/i);
|
|
1136
|
+
},
|
|
715
1137
|
);
|
|
716
1138
|
} finally {
|
|
1139
|
+
for (const worktree of retained?.worktrees ?? []) fs.rmSync(worktree.path, { recursive: true, force: true });
|
|
717
1140
|
cleanupRepo(repoDir);
|
|
718
1141
|
}
|
|
719
1142
|
});
|
|
720
1143
|
|
|
721
|
-
it("excludes hook-created synthetic files from captured patch output", { skip: hookScriptSkip }, () => {
|
|
1144
|
+
it("excludes hook-created synthetic files from captured patch output", { skip: hookScriptSkip }, async () => {
|
|
722
1145
|
const repoDir = createRepo("pi-worktree-hook-diff-");
|
|
723
1146
|
const hookPath = createHookScript(repoDir, "setup-copy-hook.mjs", `
|
|
724
1147
|
import * as fs from "node:fs";
|
|
@@ -730,7 +1153,7 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [".env.local"] }));
|
|
|
730
1153
|
|
|
731
1154
|
let setup: WorktreeSetup | undefined;
|
|
732
1155
|
try {
|
|
733
|
-
setup = createWorktrees(repoDir, `hook-diff-${process.pid}`, 1, {
|
|
1156
|
+
setup = await createWorktrees(repoDir, `hook-diff-${process.pid}`, 1, {
|
|
734
1157
|
setupHook: { hookPath: path.relative(repoDir, hookPath) },
|
|
735
1158
|
});
|
|
736
1159
|
fs.writeFileSync(path.join(setup.worktrees[0]!.path, "tracked.txt"), "modified-by-agent\n", "utf-8");
|
|
@@ -744,9 +1167,11 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [".env.local"] }));
|
|
|
744
1167
|
}
|
|
745
1168
|
});
|
|
746
1169
|
|
|
747
|
-
it("
|
|
1170
|
+
it("retains created worktrees when a later hook failure has unknown settlement", { skip: hookScriptSkip }, async () => {
|
|
1171
|
+
if (await runPoisonCaseInChild("retains created worktrees when a later hook failure has unknown settlement")) return;
|
|
748
1172
|
const repoDir = createRepo("pi-worktree-hook-cleanup-");
|
|
749
1173
|
const runId = `hook-cleanup-${Date.now().toString(36)}`;
|
|
1174
|
+
let retained: WorktreeSetup | undefined;
|
|
750
1175
|
const hookPath = createHookScript(repoDir, "flaky-hook.mjs", `
|
|
751
1176
|
import * as fs from "node:fs";
|
|
752
1177
|
const payload = JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
@@ -757,18 +1182,29 @@ if (payload.index === 1) {
|
|
|
757
1182
|
process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
|
|
758
1183
|
`);
|
|
759
1184
|
try {
|
|
760
|
-
assert.
|
|
1185
|
+
await assert.rejects(
|
|
761
1186
|
() => createWorktrees(repoDir, runId, 2, { setupHook: { hookPath: path.relative(repoDir, hookPath) } }),
|
|
762
|
-
|
|
1187
|
+
(error: unknown) => {
|
|
1188
|
+
assert.ok(error instanceof WorktreeSetupError);
|
|
1189
|
+
retained = error.snapshot.setup;
|
|
1190
|
+
assertRetainedHookFailure(error, /worktree setup hook failed/i);
|
|
1191
|
+
assert.equal(error.snapshot.attempts[1]?.hookCommand?.result?.status, 1);
|
|
1192
|
+
assert.equal(error.snapshot.setup.worktrees.length, 2);
|
|
1193
|
+
for (const worktree of error.snapshot.setup.worktrees) {
|
|
1194
|
+
assert.equal(fs.existsSync(worktree.path), true);
|
|
1195
|
+
assert.equal(git(worktree.path, ["branch", "--show-current"]), worktree.branch);
|
|
1196
|
+
}
|
|
1197
|
+
return true;
|
|
1198
|
+
},
|
|
763
1199
|
);
|
|
764
|
-
|
|
765
|
-
assert.equal(branchList.trim(), "", "temporary branches should be cleaned up after setup failure");
|
|
1200
|
+
await assert.rejects(() => createWorktrees(repoDir, "after-hook-failure", 1, { provider: "native" }), /Worktree mutation blocked/i);
|
|
766
1201
|
} finally {
|
|
1202
|
+
for (const worktree of retained?.worktrees ?? []) fs.rmSync(worktree.path, { recursive: true, force: true });
|
|
767
1203
|
cleanupRepo(repoDir);
|
|
768
1204
|
}
|
|
769
1205
|
});
|
|
770
1206
|
|
|
771
|
-
it("fails when the hook exceeds the configured timeout", { skip: hookScriptSkip }, () => {
|
|
1207
|
+
it("fails when the hook exceeds the configured timeout", { skip: hookScriptSkip }, async () => {
|
|
772
1208
|
const repoDir = createRepo("pi-worktree-hook-timeout-");
|
|
773
1209
|
const hookPath = createHookScript(repoDir, "slow-hook.mjs", `
|
|
774
1210
|
import * as fs from "node:fs";
|
|
@@ -779,11 +1215,16 @@ setTimeout(() => {
|
|
|
779
1215
|
`);
|
|
780
1216
|
const runId = `hook-timeout-${Date.now().toString(36)}`;
|
|
781
1217
|
try {
|
|
782
|
-
assert.
|
|
1218
|
+
await assert.rejects(
|
|
783
1219
|
() => createWorktrees(repoDir, runId, 1, {
|
|
784
1220
|
setupHook: { hookPath: path.relative(repoDir, hookPath), timeoutMs: 50 },
|
|
785
1221
|
}),
|
|
786
|
-
|
|
1222
|
+
(error: unknown) => {
|
|
1223
|
+
assert.ok(error instanceof WorktreeSetupError);
|
|
1224
|
+
assert.ok(error.cause instanceof Error && error.cause.cause instanceof Error);
|
|
1225
|
+
assert.match(error.cause.cause.message, /timed out/i);
|
|
1226
|
+
return true;
|
|
1227
|
+
},
|
|
787
1228
|
);
|
|
788
1229
|
} finally {
|
|
789
1230
|
cleanupRepo(repoDir);
|
|
@@ -824,4 +1265,4 @@ describe("manifest-backed worktree discard", () => {
|
|
|
824
1265
|
fs.rmSync(root, { recursive: true, force: true });
|
|
825
1266
|
}
|
|
826
1267
|
});
|
|
827
|
-
});
|
|
1268
|
+
});
|