@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,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { runSetupCommand, type SetupCommandOptions, type SetupCommandResult } from "./worktree-setup-command.ts";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import * as os from "node:os";
|
|
5
6
|
import * as path from "node:path";
|
|
@@ -9,6 +10,7 @@ import { getAgentDir } from "../../shared/utils.ts";
|
|
|
9
10
|
import type { ManagedWorktreeProvider, WorktreeNaming, WorktreeProvider } from "../../shared/types.ts";
|
|
10
11
|
|
|
11
12
|
export const DEFAULT_WORKTREE_PROVIDER: WorktreeProvider = "auto";
|
|
13
|
+
export const DEFAULT_WORKTREE_BASE_REF = "HEAD";
|
|
12
14
|
export const DEFAULT_WORKTREE_BRANCH_PREFIX = "pi-subagents/";
|
|
13
15
|
/** Internal marker used to defer Worktrunk-dependent instruction paths to launch time. */
|
|
14
16
|
export const WORKTREE_AGENT_CWD_PLACEHOLDER = path.join(path.parse(process.cwd()).root, "__pi_subagents_worktree_cwd__");
|
|
@@ -16,6 +18,9 @@ const WORKTREE_NAMING_COMPONENT_MAX_BYTES = 96;
|
|
|
16
18
|
const WORKTREE_NAMING_LABEL_MAX_BYTES = 256;
|
|
17
19
|
const WORKTREE_NAMING_BRANCH_MAX_BYTES = 256;
|
|
18
20
|
const WORKTREE_COMMAND_OUTPUT_MAX_BYTES = 128 * 1024;
|
|
21
|
+
export const MACHINE_DIFF_OPTIONS = ["--no-color", "--no-ext-diff", "--no-textconv", "--default-prefix", "--line-prefix=", "--no-relative"] as const;
|
|
22
|
+
const MACHINE_PATCH_OPTIONS = [...MACHINE_DIFF_OPTIONS, "--binary"] as const;
|
|
23
|
+
const PATCH_VALIDATION_OPTIONS = ["apply", "--check", "--cached", "--reverse", "--binary", "--whitespace=nowarn"] as const;
|
|
19
24
|
|
|
20
25
|
export interface WorktreeNamingInput {
|
|
21
26
|
runId: string;
|
|
@@ -114,6 +119,10 @@ interface WorktreeSetupHookConfig {
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
export interface CreateWorktreesOptions {
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
/** Existing absolute run deadline only; setup does not start a child budget. */
|
|
124
|
+
deadlineAt?: number;
|
|
125
|
+
onProgress?: (snapshot: WorktreeSetupProgress) => void;
|
|
117
126
|
agents?: string[];
|
|
118
127
|
/** Optional stable labels used to make branch identity readable. */
|
|
119
128
|
labels?: Array<string | undefined>;
|
|
@@ -121,12 +130,12 @@ export interface CreateWorktreesOptions {
|
|
|
121
130
|
tasks?: Array<string | undefined>;
|
|
122
131
|
/** Worktree allocator selection; auto prefers Worktrunk when available. */
|
|
123
132
|
provider?: WorktreeProvider;
|
|
133
|
+
/** Git ref used as the worktree base; defaults to `HEAD`. */
|
|
134
|
+
baseRef?: string;
|
|
124
135
|
/** Branch namespace; defaults to `pi-subagents/`. */
|
|
125
136
|
branchPrefix?: string;
|
|
126
137
|
setupHook?: WorktreeSetupHookConfig;
|
|
127
138
|
baseDir?: string;
|
|
128
|
-
/** Called with deterministic ownership metadata before setup hooks and child launch; native reports planned paths before allocation, while Worktrunk reports its returned paths after allocation. */
|
|
129
|
-
beforeCreate?: (setup: WorktreeSetup) => void;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
interface ResolvedWorktreeSetupHook {
|
|
@@ -164,8 +173,107 @@ interface RepoState {
|
|
|
164
173
|
|
|
165
174
|
const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
|
|
166
175
|
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
/** In-memory evidence only. Callers project this into existing handoff diagnostics. */
|
|
177
|
+
export interface WorktreeSetupProgress {
|
|
178
|
+
setup: WorktreeSetup;
|
|
179
|
+
attempts: Array<{ index: number; branch: string; path?: string; validated: boolean; command?: WorktreeSetupProgress["command"]; hookCommand?: WorktreeSetupProgress["command"] }>;
|
|
180
|
+
phase: string;
|
|
181
|
+
command?: { command: string; args: string[]; pid?: number; processGroupId?: number; result?: Omit<SetupCommandResult, "stdout" | "stderr"> };
|
|
182
|
+
unknown?: string;
|
|
183
|
+
cleanup?: WorktreeCleanupReport;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class WorktreeSetupError extends Error {
|
|
187
|
+
readonly snapshot: WorktreeSetupProgress;
|
|
188
|
+
constructor(error: unknown, snapshot: WorktreeSetupProgress) {
|
|
189
|
+
super(`${error instanceof Error ? error.message : String(error)}${snapshot.unknown ? `; setup settlement unknown: ${snapshot.unknown}; manual reconciliation required` : ""}`, { cause: error });
|
|
190
|
+
this.name = "WorktreeSetupError";
|
|
191
|
+
this.snapshot = snapshot;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let worktreeTurn: Promise<void> = Promise.resolve();
|
|
196
|
+
let setupActive = false;
|
|
197
|
+
let setupPoison: string | undefined;
|
|
198
|
+
|
|
199
|
+
function assertWorktreeMutationAllowed(): void {
|
|
200
|
+
if (setupPoison) throw new Error(`Worktree mutation blocked: ${setupPoison}`);
|
|
201
|
+
if (setupActive) throw new Error("Worktree setup is active; finalization must await withWorktreeTransaction");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Later async finalization owners wrap their synchronous diff/cleanup as ONE turn. */
|
|
205
|
+
export async function withWorktreeTransaction<T>(action: () => T | Promise<T>): Promise<T> {
|
|
206
|
+
const previous = worktreeTurn;
|
|
207
|
+
let release!: () => void;
|
|
208
|
+
worktreeTurn = new Promise<void>((resolve) => { release = resolve; });
|
|
209
|
+
await previous;
|
|
210
|
+
try {
|
|
211
|
+
if (setupPoison) throw new Error(`Worktree mutation blocked: ${setupPoison}`);
|
|
212
|
+
return await action();
|
|
213
|
+
} finally { release(); }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
class SetupTransaction {
|
|
217
|
+
readonly progress: WorktreeSetupProgress;
|
|
218
|
+
readonly options: CreateWorktreesOptions;
|
|
219
|
+
constructor(cwd: string, options: CreateWorktreesOptions) {
|
|
220
|
+
this.options = options;
|
|
221
|
+
this.progress = { setup: { cwd, baseCommit: "", worktrees: [] }, attempts: [], phase: "preflight" };
|
|
222
|
+
}
|
|
223
|
+
snapshot(): WorktreeSetupProgress {
|
|
224
|
+
return structuredClone(this.progress);
|
|
225
|
+
}
|
|
226
|
+
publish(): void { this.options.onProgress?.(this.snapshot()); }
|
|
227
|
+
check(): void {
|
|
228
|
+
if (setupPoison) throw new Error(setupPoison);
|
|
229
|
+
if (this.options.signal?.aborted) throw new Error("Worktree setup aborted");
|
|
230
|
+
if (this.options.deadlineAt !== undefined && Date.now() >= this.options.deadlineAt) throw new Error("Worktree setup deadline exceeded");
|
|
231
|
+
}
|
|
232
|
+
unknown(error: unknown): void {
|
|
233
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
234
|
+
this.progress.unknown ??= reason;
|
|
235
|
+
setupPoison ??= reason;
|
|
236
|
+
}
|
|
237
|
+
async command(command: string, args: string[], options: SetupCommandOptions = {}): Promise<SetupCommandResult> {
|
|
238
|
+
this.check();
|
|
239
|
+
this.progress.command = { command, args };
|
|
240
|
+
if (this.progress.phase === "allocation") this.progress.attempts.at(-1)!.command = this.progress.command;
|
|
241
|
+
if (this.progress.phase === "hook") this.progress.attempts.find((attempt) => attempt.path === options.cwd)!.hookCommand = this.progress.command;
|
|
242
|
+
const result = await runSetupCommand(command, args, {
|
|
243
|
+
...options, signal: this.options.signal, deadlineAt: this.options.deadlineAt,
|
|
244
|
+
onSpawn: (process) => { Object.assign(this.progress.command!, process); this.publish(); },
|
|
245
|
+
});
|
|
246
|
+
const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
|
|
247
|
+
this.progress.command.result = metadata;
|
|
248
|
+
if (result.processTree?.state === "unknown") this.unknown(result.error ?? "Command tree settlement unverified");
|
|
249
|
+
this.publish();
|
|
250
|
+
if (result.error) throw result.error;
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
async git(cwd: string, args: string[], acceptedExitCodes?: readonly number[]): Promise<SetupCommandResult> {
|
|
254
|
+
return this.command("git", ["-C", cwd, ...args], { acceptedExitCodes });
|
|
255
|
+
}
|
|
256
|
+
async gitChecked(cwd: string, args: string[]): Promise<string> {
|
|
257
|
+
const result = await this.git(cwd, args);
|
|
258
|
+
return result.stdout;
|
|
259
|
+
}
|
|
260
|
+
attempt(index: number, branch: string, worktreePath?: string): void {
|
|
261
|
+
this.check();
|
|
262
|
+
this.progress.phase = "allocation";
|
|
263
|
+
this.progress.command = undefined;
|
|
264
|
+
this.progress.attempts.push({ index, branch, path: worktreePath, validated: false });
|
|
265
|
+
this.publish();
|
|
266
|
+
}
|
|
267
|
+
validated(worktree: WorktreeInfo): void {
|
|
268
|
+
this.progress.phase = "validated";
|
|
269
|
+
Object.assign(this.progress.attempts.at(-1)!, { path: worktree.path, validated: true });
|
|
270
|
+
this.progress.setup.worktrees.push(worktree);
|
|
271
|
+
this.publish();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function runGit(cwd: string, args: string[], env?: NodeJS.ProcessEnv): GitResult {
|
|
276
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", windowsHide: true, shell: false, ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
169
277
|
return {
|
|
170
278
|
stdout: result.stdout ?? "",
|
|
171
279
|
stderr: result.stderr ?? "",
|
|
@@ -183,28 +291,78 @@ function runGitChecked(cwd: string, args: string[]): string {
|
|
|
183
291
|
return result.stdout;
|
|
184
292
|
}
|
|
185
293
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
294
|
+
/** Validate a captured patch against the worktree index without changing either. */
|
|
295
|
+
export function validateWorktreePatch(worktreePath: string, patchPath: string): string | undefined {
|
|
296
|
+
const result = runGit(worktreePath, [...PATCH_VALIDATION_OPTIONS, patchPath]);
|
|
297
|
+
if (result.status === 0) return undefined;
|
|
298
|
+
return result.stderr.trim() || result.stdout.trim() || `git -C ${worktreePath} apply --check failed`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function currentWorktreePatch(worktreePath: string, baseCommit: string): { patch: string } | { error: string } {
|
|
302
|
+
assertWorktreeMutationAllowed();
|
|
303
|
+
let tempDir: string;
|
|
304
|
+
try {
|
|
305
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-index-"));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
192
308
|
}
|
|
309
|
+
const env = { GIT_INDEX_FILE: path.join(tempDir, "index") };
|
|
310
|
+
try {
|
|
311
|
+
const readTree = runGit(worktreePath, ["read-tree", "HEAD"], env);
|
|
312
|
+
if (readTree.status !== 0) return { error: readTree.stderr.trim() || readTree.stdout.trim() || "git read-tree failed" };
|
|
313
|
+
const add = runGit(worktreePath, ["add", "-A"], env);
|
|
314
|
+
if (add.status !== 0) return { error: add.stderr.trim() || add.stdout.trim() || "git add failed" };
|
|
315
|
+
const diff = runGit(worktreePath, ["diff", "--cached", ...MACHINE_PATCH_OPTIONS, baseCommit], env);
|
|
316
|
+
if (diff.status !== 0) return { error: diff.stderr.trim() || diff.stdout.trim() || "git diff failed" };
|
|
317
|
+
return { patch: diff.stdout };
|
|
318
|
+
} finally {
|
|
319
|
+
try {
|
|
320
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
321
|
+
} catch {
|
|
322
|
+
// Cleanup safety depends on preserving the worktree, not on best-effort temp-index deletion.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function validateWorktreePatchRepresentsCurrentWorktree(worktreePath: string, baseCommit: string, patchPath: string): string | undefined {
|
|
328
|
+
const validationError = validateWorktreePatch(worktreePath, patchPath);
|
|
329
|
+
if (validationError) return validationError;
|
|
330
|
+
let capturedPatch: string;
|
|
331
|
+
try {
|
|
332
|
+
capturedPatch = fs.readFileSync(patchPath, "utf-8");
|
|
333
|
+
} catch (error) {
|
|
334
|
+
return error instanceof Error ? error.message : String(error);
|
|
335
|
+
}
|
|
336
|
+
const current = currentWorktreePatch(worktreePath, baseCommit);
|
|
337
|
+
if ("error" in current) return current.error || "failed to capture current worktree patch";
|
|
338
|
+
if (capturedPatch !== current.patch) return "captured handoff patch does not match current worktree changes";
|
|
193
339
|
return undefined;
|
|
194
340
|
}
|
|
195
341
|
|
|
196
|
-
function resolveRepoState(cwd: string): RepoState {
|
|
197
|
-
const
|
|
198
|
-
|
|
342
|
+
async function resolveRepoState(tx: SetupTransaction, cwd: string, requestedBaseRef: string | undefined): Promise<RepoState> {
|
|
343
|
+
const repoCheck = await tx.git(cwd, ["rev-parse", "--is-inside-work-tree"], [0, 128]);
|
|
344
|
+
if (repoCheck.status !== 0 || repoCheck.stdout.trim() !== "true") throw new Error("worktree isolation requires a git repository");
|
|
345
|
+
const rawPrefix = (await tx.gitChecked(cwd, ["rev-parse", "--show-prefix"])).trim();
|
|
346
|
+
const cwdRelative = rawPrefix ? path.normalize(rawPrefix.replace(/[\\/]+$/, "")) : "";
|
|
347
|
+
const toplevel = (await tx.gitChecked(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
199
348
|
|
|
200
349
|
// pi-subagents writes durable runtime state under .pi-subagents/ by default;
|
|
201
350
|
// that state must not make managed isolation unusable for later runs.
|
|
202
|
-
const status =
|
|
351
|
+
const status = await tx.gitChecked(toplevel, ["status", "--porcelain", "--", `:!${PROJECT_SUBAGENTS_RELATIVE_DIR}`]);
|
|
203
352
|
if (status.trim().length > 0) {
|
|
204
353
|
throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first.");
|
|
205
354
|
}
|
|
206
355
|
|
|
207
|
-
const
|
|
356
|
+
const baseRef = normalizeWorktreeBaseRef(requestedBaseRef) ?? DEFAULT_WORKTREE_BASE_REF;
|
|
357
|
+
let baseCommit: string;
|
|
358
|
+
try {
|
|
359
|
+
const base = await tx.git(toplevel, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`], [0, 128]);
|
|
360
|
+
if (base.status !== 0) throw new Error(base.stderr.trim() || "ref not found");
|
|
361
|
+
baseCommit = base.stdout.trim();
|
|
362
|
+
} catch (error) {
|
|
363
|
+
throw new Error(`baseRef '${baseRef}' could not be resolved to a commit: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
|
|
364
|
+
}
|
|
365
|
+
if (!baseCommit) throw new Error(`baseRef '${baseRef}' could not be resolved to a commit`);
|
|
208
366
|
return { toplevel, cwdRelative, baseCommit };
|
|
209
367
|
}
|
|
210
368
|
|
|
@@ -214,7 +372,13 @@ function normalizeComparableCwd(cwd: string): string {
|
|
|
214
372
|
const missingSegments: string[] = [];
|
|
215
373
|
while (true) {
|
|
216
374
|
try {
|
|
217
|
-
|
|
375
|
+
let realpath: string;
|
|
376
|
+
try {
|
|
377
|
+
realpath = fs.realpathSync.native(existing);
|
|
378
|
+
} catch {
|
|
379
|
+
realpath = fs.realpathSync(existing);
|
|
380
|
+
}
|
|
381
|
+
return path.join(realpath, ...missingSegments.reverse());
|
|
218
382
|
} catch {
|
|
219
383
|
const parent = path.dirname(existing);
|
|
220
384
|
if (parent === existing) return resolved;
|
|
@@ -279,11 +443,19 @@ export function sanitizeWorktreePathComponent(value: string, maxBytes = WORKTREE
|
|
|
279
443
|
}
|
|
280
444
|
|
|
281
445
|
function validGitRef(ref: string): boolean {
|
|
282
|
-
if (!ref || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
283
|
-
if (
|
|
446
|
+
if (!ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
447
|
+
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
448
|
+
if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
|
|
284
449
|
return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
|
|
285
450
|
}
|
|
286
451
|
|
|
452
|
+
/** Normalize and validate a configured worktree base ref without resolving it. */
|
|
453
|
+
export function normalizeWorktreeBaseRef(value: unknown): string | undefined {
|
|
454
|
+
if (value === undefined) return undefined;
|
|
455
|
+
if (typeof value !== "string" || !validGitRef(value)) throw new Error("baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.");
|
|
456
|
+
return value;
|
|
457
|
+
}
|
|
458
|
+
|
|
287
459
|
/** Normalize and validate the configured Git branch namespace. */
|
|
288
460
|
export function normalizeWorktreeBranchPrefix(value: string | undefined): string {
|
|
289
461
|
const raw = value === undefined ? DEFAULT_WORKTREE_BRANCH_PREFIX : value.trim();
|
|
@@ -359,11 +531,6 @@ interface WorktrunkCapability {
|
|
|
359
531
|
reason?: string;
|
|
360
532
|
}
|
|
361
533
|
|
|
362
|
-
interface SourceCheckoutSnapshot {
|
|
363
|
-
head: string;
|
|
364
|
-
branch?: string;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
534
|
function runWorktrunk(args: string[], cwd?: string): WorktreeCommandResult {
|
|
368
535
|
try {
|
|
369
536
|
const result = spawnSync("wt", args, {
|
|
@@ -396,18 +563,6 @@ function probeWorktrunk(): WorktrunkCapability {
|
|
|
396
563
|
return { available: true };
|
|
397
564
|
}
|
|
398
565
|
|
|
399
|
-
function snapshotSourceCheckout(toplevel: string, head: string): SourceCheckoutSnapshot {
|
|
400
|
-
const branch = runGit(toplevel, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
401
|
-
const branchName = branch.status === 0 ? branch.stdout.trim() : "";
|
|
402
|
-
return branchName ? { head, branch: branchName } : { head };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function restoreSourceCheckoutIfWorktrunkSwitchedIt(toplevel: string, createdBranch: string, sourceCheckout: SourceCheckoutSnapshot): void {
|
|
406
|
-
const current = runGit(toplevel, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
407
|
-
if (current.status !== 0 || current.stdout.trim() !== createdBranch) return;
|
|
408
|
-
runGitChecked(toplevel, sourceCheckout.branch ? ["checkout", sourceCheckout.branch] : ["checkout", "--detach", sourceCheckout.head]);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
566
|
/** Resolve a requested provider without silently switching after allocation starts. */
|
|
412
567
|
export function resolveWorktreeProvider(requested: WorktreeProvider | undefined, baseDir?: string): ManagedWorktreeProvider {
|
|
413
568
|
const selection = requested ?? DEFAULT_WORKTREE_PROVIDER;
|
|
@@ -423,36 +578,117 @@ export function resolveWorktreeProvider(requested: WorktreeProvider | undefined,
|
|
|
423
578
|
return "native";
|
|
424
579
|
}
|
|
425
580
|
|
|
581
|
+
async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreeProvider | undefined, baseDir?: string): Promise<ManagedWorktreeProvider> {
|
|
582
|
+
const selection = requested ?? DEFAULT_WORKTREE_PROVIDER;
|
|
583
|
+
if (selection !== "auto" && selection !== "native" && selection !== "worktrunk") throw new Error('worktree provider must be "auto", "native", or "worktrunk"');
|
|
584
|
+
if (selection === "native") return "native";
|
|
585
|
+
if (hasConfiguredWorktreeBaseDir(baseDir)) {
|
|
586
|
+
if (selection === "worktrunk") throw new Error("worktreeProvider='worktrunk' cannot be combined with worktreeBaseDir or SELESAI_SUBAGENTS_WORKTREE_DIR");
|
|
587
|
+
return "native";
|
|
588
|
+
}
|
|
589
|
+
let reason: string | undefined;
|
|
590
|
+
try {
|
|
591
|
+
const probeExitCodes = Array.from({ length: 256 }, (_, code) => code);
|
|
592
|
+
const version = await tx.command("wt", ["--version"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
|
|
593
|
+
if (version.status !== 0 || !/\b(?:wt\s+)?v?(\d+\.\d+(?:\.\d+)?)\b/i.test(version.stdout.trim())) reason = "Worktrunk is unavailable or returned an invalid version";
|
|
594
|
+
else {
|
|
595
|
+
const help = await tx.command("wt", ["switch", "--help"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
|
|
596
|
+
const missing = ["--create", "--base", "--no-cd", "--no-hooks", "--format"].filter((flag) => !`${help.stdout}\n${help.stderr}`.includes(flag));
|
|
597
|
+
if (help.status !== 0 || missing.length) reason = `Worktrunk switch capability unavailable: ${missing.join(", ")}`;
|
|
598
|
+
}
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
601
|
+
reason = error.message;
|
|
602
|
+
}
|
|
603
|
+
if (!reason) return "worktrunk";
|
|
604
|
+
if (selection === "worktrunk") throw new Error(`Worktrunk provider is unavailable: ${reason}`);
|
|
605
|
+
return "native";
|
|
606
|
+
}
|
|
607
|
+
|
|
426
608
|
/** Whether a launch must bind its worktree-dependent paths after allocation. */
|
|
427
609
|
export function shouldDeferWorktreeCwd(requested: WorktreeProvider | undefined, baseDir?: string): boolean {
|
|
428
610
|
return (requested ?? DEFAULT_WORKTREE_PROVIDER) !== "native" && !hasConfiguredWorktreeBaseDir(baseDir);
|
|
429
611
|
}
|
|
430
612
|
|
|
431
|
-
|
|
613
|
+
/**
|
|
614
|
+
* Resolves the dedicated worktree root: the configured base directory or
|
|
615
|
+
* SELESAI_SUBAGENTS_WORKTREE_DIR when set, otherwise a `worktrees` folder sibling
|
|
616
|
+
* to the repository. Managed leaves always nest one level deeper under the
|
|
617
|
+
* project folder (`basename(repoRoot)`).
|
|
618
|
+
*/
|
|
619
|
+
function resolveWorktreeDedicatedRoot(configuredBaseDir: string | undefined, repoRoot: string): string {
|
|
432
620
|
const rawBaseDir = configuredBaseDir ?? process.env.SELESAI_SUBAGENTS_WORKTREE_DIR;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
621
|
+
let expanded: string;
|
|
622
|
+
if (rawBaseDir === undefined || (configuredBaseDir === undefined && !rawBaseDir.trim())) {
|
|
623
|
+
expanded = path.join(path.dirname(repoRoot), "worktrees");
|
|
624
|
+
} else {
|
|
625
|
+
const trimmed = rawBaseDir.trim();
|
|
626
|
+
if (!trimmed) throw new Error("worktree base directory cannot be empty");
|
|
437
627
|
|
|
438
|
-
|
|
439
|
-
|
|
628
|
+
const candidate = trimmed.startsWith("~/") ? path.join(os.homedir(), trimmed.slice(2)) : trimmed;
|
|
629
|
+
expanded = path.isAbsolute(candidate) ? candidate : path.resolve(repoRoot, candidate);
|
|
630
|
+
}
|
|
440
631
|
const extensionsDir = normalizeComparableCwd(path.join(getAgentDir(), "extensions"));
|
|
441
|
-
|
|
442
|
-
if (!relativeToExtensions || (!relativeToExtensions.startsWith(`..${path.sep}`) && relativeToExtensions !== ".." && !path.isAbsolute(relativeToExtensions))) {
|
|
632
|
+
if (isPathInside(extensionsDir, normalizeComparableCwd(expanded))) {
|
|
443
633
|
throw new Error(`worktree base directory cannot be inside Pi extensions directory: ${extensionsDir}. Choose a directory outside it.`);
|
|
444
634
|
}
|
|
635
|
+
return expanded;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function buildNativeProjectPath(dedicatedRoot: string, repoRoot: string): string {
|
|
639
|
+
return path.join(dedicatedRoot, path.basename(repoRoot));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Creates the project folder (parents included) so `git worktree add` can create
|
|
644
|
+
* the leaf inside it. Must only run after `assertSafeWorktreeLocation` accepted
|
|
645
|
+
* the planned leaf — an unsafe base must never be materialized on disk.
|
|
646
|
+
*/
|
|
647
|
+
function ensureProjectWorktreeDir(dedicatedRoot: string, repoRoot: string): void {
|
|
445
648
|
try {
|
|
446
|
-
fs.mkdirSync(
|
|
649
|
+
fs.mkdirSync(buildNativeProjectPath(dedicatedRoot, repoRoot), { recursive: true });
|
|
447
650
|
} catch (error) {
|
|
448
651
|
const message = error instanceof Error ? error.message : String(error);
|
|
449
|
-
throw new Error(`failed to create worktree base directory ${
|
|
652
|
+
throw new Error(`failed to create worktree base directory ${dedicatedRoot}: ${message}`);
|
|
450
653
|
}
|
|
451
|
-
return resolved;
|
|
452
654
|
}
|
|
453
655
|
|
|
454
|
-
function buildNativeWorktreePath(
|
|
455
|
-
return path.join(
|
|
656
|
+
function buildNativeWorktreePath(dedicatedRoot: string, repoRoot: string, runId: string, index: number): string {
|
|
657
|
+
return path.join(buildNativeProjectPath(dedicatedRoot, repoRoot), `pi-worktree-${sanitizeWorktreePathComponent(runId, 120)}-${index}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function isPathInside(parent: string, child: string): boolean {
|
|
661
|
+
const relative = path.relative(parent, child);
|
|
662
|
+
if (!relative || relative === ".") return true;
|
|
663
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function isStrictChildPath(parent: string, child: string): boolean {
|
|
667
|
+
const relative = path.relative(parent, child);
|
|
668
|
+
return Boolean(relative) && relative !== "." && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Fail closed before `git worktree add`: reject unsafe planned/realpath locations. */
|
|
672
|
+
function assertSafeWorktreeLocation(worktreePath: string, repoRoot: string, dedicatedRoot: string): void {
|
|
673
|
+
const resolvedLeaf = normalizeComparableCwd(worktreePath);
|
|
674
|
+
const resolvedRepoRoot = normalizeComparableCwd(repoRoot);
|
|
675
|
+
const projectDir = normalizeComparableCwd(buildNativeProjectPath(dedicatedRoot, repoRoot));
|
|
676
|
+
const repoParent = normalizeComparableCwd(path.dirname(resolvedRepoRoot));
|
|
677
|
+
const leafParent = normalizeComparableCwd(path.dirname(resolvedLeaf));
|
|
678
|
+
const extensionsDir = normalizeComparableCwd(path.join(getAgentDir(), "extensions"));
|
|
679
|
+
|
|
680
|
+
if (isPathInside(extensionsDir, projectDir) || isPathInside(extensionsDir, resolvedLeaf)) {
|
|
681
|
+
throw new Error(`worktree path cannot be inside Pi extensions directory: ${extensionsDir}. Choose a directory outside it.`);
|
|
682
|
+
}
|
|
683
|
+
if (isPathInside(resolvedRepoRoot, resolvedLeaf)) {
|
|
684
|
+
throw new Error(`worktree path would land inside the repository checkout: ${resolvedLeaf}`);
|
|
685
|
+
}
|
|
686
|
+
if (leafParent === repoParent) {
|
|
687
|
+
throw new Error(`worktree path would be a direct child of the repository parent: ${resolvedLeaf}`);
|
|
688
|
+
}
|
|
689
|
+
if (!isStrictChildPath(projectDir, resolvedLeaf)) {
|
|
690
|
+
throw new Error(`worktree path must be a strict child of the project worktree directory ${projectDir}: ${resolvedLeaf}`);
|
|
691
|
+
}
|
|
456
692
|
}
|
|
457
693
|
|
|
458
694
|
function resolveRepoCwdRelative(cwd: string): string {
|
|
@@ -470,7 +706,9 @@ function resolveRepoCwdRelative(cwd: string): string {
|
|
|
470
706
|
export function resolveExpectedWorktreeAgentCwd(cwd: string, runId: string, index: number, baseDir?: string): string {
|
|
471
707
|
const cwdRelative = resolveRepoCwdRelative(cwd);
|
|
472
708
|
const repoRoot = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
473
|
-
const
|
|
709
|
+
const dedicatedRoot = resolveWorktreeDedicatedRoot(baseDir, repoRoot);
|
|
710
|
+
const worktreePath = buildNativeWorktreePath(dedicatedRoot, repoRoot, runId, index);
|
|
711
|
+
assertSafeWorktreeLocation(worktreePath, repoRoot, dedicatedRoot);
|
|
474
712
|
return cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath;
|
|
475
713
|
}
|
|
476
714
|
|
|
@@ -544,11 +782,6 @@ function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
|
|
|
544
782
|
return path.normalize(relative);
|
|
545
783
|
}
|
|
546
784
|
|
|
547
|
-
function hasTrackedEntries(worktreePath: string, relativePath: string): boolean {
|
|
548
|
-
const result = runGit(worktreePath, ["ls-files", "--", relativePath]);
|
|
549
|
-
return result.status === 0 && result.stdout.trim().length > 0;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
785
|
function parseWorktreeSetupHookOutput(rawStdout: string): WorktreeSetupHookOutput {
|
|
553
786
|
const trimmed = rawStdout.trim();
|
|
554
787
|
if (!trimmed) {
|
|
@@ -567,53 +800,49 @@ function parseWorktreeSetupHookOutput(rawStdout: string): WorktreeSetupHookOutpu
|
|
|
567
800
|
return parsed as WorktreeSetupHookOutput;
|
|
568
801
|
}
|
|
569
802
|
|
|
570
|
-
function runWorktreeSetupHook(
|
|
803
|
+
async function runWorktreeSetupHook(
|
|
804
|
+
tx: SetupTransaction,
|
|
571
805
|
hook: ResolvedWorktreeSetupHook,
|
|
572
806
|
input: WorktreeSetupHookInput,
|
|
573
|
-
): string[] {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const code = "code" in result.error ? result.error.code : undefined;
|
|
585
|
-
if (code === "ETIMEDOUT") {
|
|
586
|
-
throw new Error(`worktree setup hook timed out after ${hook.timeoutMs}ms`);
|
|
587
|
-
}
|
|
588
|
-
throw new Error(`worktree setup hook failed: ${result.error.message}`);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (result.status !== 0) {
|
|
592
|
-
const details = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
593
|
-
throw new Error(`worktree setup hook failed with exit code ${result.status}: ${details}`);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const output = parseWorktreeSetupHookOutput(result.stdout);
|
|
597
|
-
if (output.syntheticPaths === undefined) return [];
|
|
598
|
-
if (!Array.isArray(output.syntheticPaths)) {
|
|
599
|
-
throw new Error("worktree setup hook output field 'syntheticPaths' must be an array of relative paths");
|
|
807
|
+
): Promise<string[]> {
|
|
808
|
+
tx.progress.phase = "hook";
|
|
809
|
+
let result: SetupCommandResult;
|
|
810
|
+
try {
|
|
811
|
+
result = await tx.command(hook.hookPath, [], {
|
|
812
|
+
cwd: input.worktreePath,
|
|
813
|
+
input: JSON.stringify(input),
|
|
814
|
+
hookTimeoutMs: hook.timeoutMs,
|
|
815
|
+
});
|
|
816
|
+
} catch (error) {
|
|
817
|
+
throw new Error(`worktree setup hook failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
600
818
|
}
|
|
819
|
+
tx.progress.phase = "hook-validation";
|
|
601
820
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (
|
|
605
|
-
|
|
821
|
+
try {
|
|
822
|
+
const output = parseWorktreeSetupHookOutput(result.stdout);
|
|
823
|
+
if (output.syntheticPaths === undefined) return [];
|
|
824
|
+
if (!Array.isArray(output.syntheticPaths)) {
|
|
825
|
+
throw new Error("worktree setup hook output field 'syntheticPaths' must be an array of relative paths");
|
|
606
826
|
}
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
throw new Error(
|
|
827
|
+
const uniquePaths = new Set<string>();
|
|
828
|
+
for (const candidate of output.syntheticPaths) {
|
|
829
|
+
if (typeof candidate !== "string") throw new Error("worktree setup hook output field 'syntheticPaths' must contain only strings");
|
|
830
|
+
const normalizedPath = normalizeSyntheticPath(input.worktreePath, candidate);
|
|
831
|
+
if ((await tx.gitChecked(input.worktreePath, ["ls-files", "--", normalizedPath])).trim()) {
|
|
832
|
+
throw new Error(`worktree setup hook cannot mark tracked paths as synthetic: ${normalizedPath}`);
|
|
833
|
+
}
|
|
834
|
+
uniquePaths.add(normalizedPath);
|
|
610
835
|
}
|
|
611
|
-
uniquePaths
|
|
836
|
+
return [...uniquePaths];
|
|
837
|
+
} catch (error) {
|
|
838
|
+
// Semantic rejection occurs after exit: retain uncertainty, never signal an old PID.
|
|
839
|
+
tx.unknown(error);
|
|
840
|
+
throw error;
|
|
612
841
|
}
|
|
613
|
-
return [...uniquePaths];
|
|
614
842
|
}
|
|
615
843
|
|
|
616
|
-
function finalizeCreatedWorktree(
|
|
844
|
+
async function finalizeCreatedWorktree(
|
|
845
|
+
tx: SetupTransaction,
|
|
617
846
|
toplevel: string,
|
|
618
847
|
cwdRelative: string,
|
|
619
848
|
runId: string,
|
|
@@ -621,38 +850,23 @@ function finalizeCreatedWorktree(
|
|
|
621
850
|
setupHook: ResolvedWorktreeSetupHook | undefined,
|
|
622
851
|
agent: string | undefined,
|
|
623
852
|
worktree: WorktreeInfo,
|
|
624
|
-
): WorktreeInfo {
|
|
853
|
+
): Promise<WorktreeInfo> {
|
|
625
854
|
const agentCwd = cwdRelative ? path.join(worktree.path, cwdRelative) : worktree.path;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
branch: worktree.branch,
|
|
636
|
-
index: worktree.index,
|
|
637
|
-
runId,
|
|
638
|
-
baseCommit,
|
|
639
|
-
agent,
|
|
640
|
-
});
|
|
641
|
-
syntheticPaths.push(...hookSyntheticPaths);
|
|
642
|
-
}
|
|
643
|
-
return { ...worktree, agentCwd, nodeModulesLinked, syntheticPaths };
|
|
644
|
-
} catch (error) {
|
|
645
|
-
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
646
|
-
// Best-effort rollback; preserve the original setup failure.
|
|
647
|
-
}
|
|
648
|
-
try { runGitChecked(toplevel, ["branch", "-D", worktree.branch]); } catch {
|
|
649
|
-
// Best-effort rollback; preserve the original setup failure.
|
|
650
|
-
}
|
|
651
|
-
throw error;
|
|
855
|
+
tx.check();
|
|
856
|
+
const nodeModulesLinked = linkNodeModulesIfPresent(toplevel, worktree.path);
|
|
857
|
+
const syntheticPaths = nodeModulesLinked ? ["node_modules"] : [];
|
|
858
|
+
if (setupHook) {
|
|
859
|
+
const hookSyntheticPaths = await runWorktreeSetupHook(tx, setupHook, {
|
|
860
|
+
version: 1, repoRoot: toplevel, worktreePath: worktree.path, agentCwd,
|
|
861
|
+
branch: worktree.branch, index: worktree.index, runId, baseCommit, agent,
|
|
862
|
+
});
|
|
863
|
+
syntheticPaths.push(...hookSyntheticPaths);
|
|
652
864
|
}
|
|
865
|
+
return { ...worktree, agentCwd, nodeModulesLinked, syntheticPaths };
|
|
653
866
|
}
|
|
654
867
|
|
|
655
|
-
function createNativeWorktree(
|
|
868
|
+
async function createNativeWorktree(
|
|
869
|
+
tx: SetupTransaction,
|
|
656
870
|
toplevel: string,
|
|
657
871
|
cwdRelative: string,
|
|
658
872
|
runId: string,
|
|
@@ -660,19 +874,24 @@ function createNativeWorktree(
|
|
|
660
874
|
baseCommit: string,
|
|
661
875
|
setupHook: ResolvedWorktreeSetupHook | undefined,
|
|
662
876
|
agent: string | undefined,
|
|
663
|
-
|
|
877
|
+
dedicatedRoot: string,
|
|
664
878
|
labels: Array<string | undefined> | undefined,
|
|
665
879
|
tasks: Array<string | undefined> | undefined,
|
|
666
880
|
branchPrefix: string | undefined,
|
|
667
|
-
): WorktreeInfo {
|
|
881
|
+
): Promise<WorktreeInfo> {
|
|
668
882
|
const naming = buildWorktreeNaming({ runId, index, agent, label: labels?.[index], task: tasks?.[index], branchPrefix });
|
|
669
|
-
const worktreePath = buildNativeWorktreePath(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}
|
|
675
|
-
|
|
883
|
+
const worktreePath = buildNativeWorktreePath(dedicatedRoot, toplevel, runId, index);
|
|
884
|
+
assertSafeWorktreeLocation(worktreePath, toplevel, dedicatedRoot);
|
|
885
|
+
// Absence is required before claiming any partial artifacts from this attempt.
|
|
886
|
+
const branch = await tx.git(toplevel, ["show-ref", "--verify", "--quiet", `refs/heads/${naming.requestedBranch}`], [0, 1]);
|
|
887
|
+
let pathExists = false;
|
|
888
|
+
try { fs.lstatSync(worktreePath); pathExists = true; }
|
|
889
|
+
catch (error) { if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error; }
|
|
890
|
+
if (branch.status !== 1 || pathExists) throw new Error(`worktree path or branch already exists: ${worktreePath}, ${naming.requestedBranch}`);
|
|
891
|
+
tx.attempt(index, naming.requestedBranch, worktreePath);
|
|
892
|
+
ensureProjectWorktreeDir(dedicatedRoot, toplevel);
|
|
893
|
+
await tx.git(toplevel, ["worktree", "add", worktreePath, "-b", naming.requestedBranch, baseCommit]);
|
|
894
|
+
const worktree: WorktreeInfo = {
|
|
676
895
|
path: worktreePath,
|
|
677
896
|
agentCwd: worktreePath,
|
|
678
897
|
branch: naming.requestedBranch,
|
|
@@ -681,7 +900,9 @@ function createNativeWorktree(
|
|
|
681
900
|
syntheticPaths: [],
|
|
682
901
|
provider: "native",
|
|
683
902
|
naming,
|
|
684
|
-
}
|
|
903
|
+
};
|
|
904
|
+
tx.validated(worktree);
|
|
905
|
+
return finalizeCreatedWorktree(tx, toplevel, cwdRelative, runId, baseCommit, setupHook, agent, worktree);
|
|
685
906
|
}
|
|
686
907
|
|
|
687
908
|
interface WorktrunkSwitchOutput {
|
|
@@ -706,7 +927,8 @@ function parseWorktrunkSwitchOutput(rawStdout: string): WorktrunkSwitchOutput {
|
|
|
706
927
|
return parsed as WorktrunkSwitchOutput;
|
|
707
928
|
}
|
|
708
929
|
|
|
709
|
-
function createWorktrunkWorktree(
|
|
930
|
+
async function createWorktrunkWorktree(
|
|
931
|
+
tx: SetupTransaction,
|
|
710
932
|
toplevel: string,
|
|
711
933
|
cwdRelative: string,
|
|
712
934
|
runId: string,
|
|
@@ -716,27 +938,19 @@ function createWorktrunkWorktree(
|
|
|
716
938
|
labels: Array<string | undefined> | undefined,
|
|
717
939
|
tasks: Array<string | undefined> | undefined,
|
|
718
940
|
branchPrefix: string | undefined,
|
|
719
|
-
): WorktreeInfo {
|
|
941
|
+
): Promise<WorktreeInfo> {
|
|
720
942
|
const naming = buildWorktreeNaming({ runId, index, agent: agents?.[index], label: labels?.[index], task: tasks?.[index], branchPrefix });
|
|
721
|
-
const sourceCheckout = snapshotSourceCheckout(toplevel, baseCommit);
|
|
722
943
|
const args = ["-C", toplevel, "switch", "--create", naming.requestedBranch, "--base", baseCommit, "--no-cd", "--no-hooks", "--format", "json"];
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
throw new Error(`Worktrunk provisioning failed: ${message}`);
|
|
727
|
-
}
|
|
728
|
-
let createdAllocation = false;
|
|
729
|
-
let returnedPath: string | undefined;
|
|
944
|
+
tx.attempt(index, naming.requestedBranch);
|
|
945
|
+
const result = await tx.command("wt", args, { cwd: toplevel, maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES });
|
|
946
|
+
tx.progress.phase = "validation";
|
|
730
947
|
try {
|
|
731
948
|
const output = parseWorktrunkSwitchOutput(result.stdout);
|
|
732
|
-
|
|
733
|
-
if (typeof output.path === "string" && path.isAbsolute(output.path)) returnedPath = path.resolve(output.path);
|
|
734
|
-
if (!createdAllocation || output.branch !== naming.requestedBranch || output.base_branch !== baseCommit) {
|
|
949
|
+
if (output.action !== "created" || output.created_branch !== true || output.branch !== naming.requestedBranch || output.base_branch !== baseCommit) {
|
|
735
950
|
throw new Error("Worktrunk provisioning returned inconsistent creation metadata");
|
|
736
951
|
}
|
|
737
952
|
if (typeof output.path !== "string" || !path.isAbsolute(output.path)) throw new Error("Worktrunk provisioning returned a non-absolute worktree path");
|
|
738
|
-
const worktreePathCandidate =
|
|
739
|
-
if (!worktreePathCandidate) throw new Error("Worktrunk provisioning returned a non-absolute worktree path");
|
|
953
|
+
const worktreePathCandidate = path.resolve(output.path);
|
|
740
954
|
let stat: fs.Stats;
|
|
741
955
|
try {
|
|
742
956
|
stat = fs.lstatSync(worktreePathCandidate);
|
|
@@ -746,16 +960,16 @@ function createWorktrunkWorktree(
|
|
|
746
960
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Worktrunk provisioning returned a path that is not a real directory");
|
|
747
961
|
const worktreePath = normalizeComparableCwd(worktreePathCandidate);
|
|
748
962
|
if (worktreePath === normalizeComparableCwd(toplevel)) throw new Error("Worktrunk provisioning returned the source checkout path");
|
|
749
|
-
const sourceCommonDirRaw =
|
|
750
|
-
const returnedCommonDirRaw =
|
|
963
|
+
const sourceCommonDirRaw = (await tx.gitChecked(toplevel, ["rev-parse", "--git-common-dir"])).trim();
|
|
964
|
+
const returnedCommonDirRaw = (await tx.gitChecked(worktreePath, ["rev-parse", "--git-common-dir"])).trim();
|
|
751
965
|
const sourceCommonDir = normalizeComparableCwd(path.isAbsolute(sourceCommonDirRaw) ? sourceCommonDirRaw : path.resolve(toplevel, sourceCommonDirRaw));
|
|
752
966
|
const returnedCommonDir = normalizeComparableCwd(path.isAbsolute(returnedCommonDirRaw) ? returnedCommonDirRaw : path.resolve(worktreePath, returnedCommonDirRaw));
|
|
753
967
|
if (returnedCommonDir !== sourceCommonDir) throw new Error("Worktrunk provisioning returned a worktree for a different repository");
|
|
754
|
-
const returnedBranch =
|
|
968
|
+
const returnedBranch = (await tx.gitChecked(worktreePath, ["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
|
|
755
969
|
if (returnedBranch !== naming.requestedBranch) throw new Error("Worktrunk provisioning returned a worktree on a different branch");
|
|
756
|
-
const returnedHead =
|
|
970
|
+
const returnedHead = (await tx.gitChecked(worktreePath, ["rev-parse", "HEAD"])).trim();
|
|
757
971
|
if (returnedHead !== baseCommit) throw new Error("Worktrunk provisioning returned a worktree at a different base commit");
|
|
758
|
-
|
|
972
|
+
const worktree: WorktreeInfo = {
|
|
759
973
|
path: worktreePath,
|
|
760
974
|
agentCwd: cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath,
|
|
761
975
|
branch: naming.requestedBranch,
|
|
@@ -765,35 +979,16 @@ function createWorktrunkWorktree(
|
|
|
765
979
|
provider: "worktrunk",
|
|
766
980
|
naming,
|
|
767
981
|
};
|
|
982
|
+
tx.validated(worktree);
|
|
983
|
+
return worktree;
|
|
768
984
|
} catch (error) {
|
|
769
|
-
|
|
770
|
-
try { restoreSourceCheckoutIfWorktrunkSwitchedIt(toplevel, naming.requestedBranch, sourceCheckout); } catch {
|
|
771
|
-
// Best-effort rollback; preserve the validation failure.
|
|
772
|
-
}
|
|
773
|
-
try {
|
|
774
|
-
const listedPath = findGitWorktreePath(toplevel, naming.requestedBranch);
|
|
775
|
-
const candidates = [listedPath, returnedPath].filter((candidate, candidateIndex, all): candidate is string => Boolean(candidate) && all.indexOf(candidate) === candidateIndex);
|
|
776
|
-
for (const candidate of candidates) {
|
|
777
|
-
if (normalizeComparableCwd(candidate) === normalizeComparableCwd(toplevel)) continue;
|
|
778
|
-
try {
|
|
779
|
-
runGitChecked(toplevel, ["worktree", "remove", "--force", candidate]);
|
|
780
|
-
break;
|
|
781
|
-
} catch {
|
|
782
|
-
// Try another provider-reported/listed path before giving up.
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
} catch {
|
|
786
|
-
// Best-effort rollback; preserve the validation failure.
|
|
787
|
-
}
|
|
788
|
-
try { runGitChecked(toplevel, ["branch", "-D", naming.requestedBranch]); } catch {
|
|
789
|
-
// Best-effort rollback; preserve the validation failure.
|
|
790
|
-
}
|
|
791
|
-
}
|
|
985
|
+
tx.unknown(error);
|
|
792
986
|
throw error;
|
|
793
987
|
}
|
|
794
988
|
}
|
|
795
989
|
|
|
796
990
|
function removeSyntheticPath(worktree: WorktreeInfo, syntheticPath: string): void {
|
|
991
|
+
assertWorktreeMutationAllowed();
|
|
797
992
|
const resolved = path.resolve(worktree.path, syntheticPath);
|
|
798
993
|
const relative = path.relative(worktree.path, resolved);
|
|
799
994
|
if (!relative || relative === "." || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
@@ -872,15 +1067,18 @@ function captureWorktreeDiff(
|
|
|
872
1067
|
): WorktreeDiff {
|
|
873
1068
|
removeSyntheticPathsBeforeDiff(worktree);
|
|
874
1069
|
runGitChecked(worktree.path, ["add", "-A"]);
|
|
875
|
-
const diffStat = runGitChecked(worktree.path, ["diff", "--cached", "--stat", setup.baseCommit]).trim();
|
|
876
|
-
const patch = runGitChecked(worktree.path, ["diff", "--cached", setup.baseCommit]);
|
|
877
|
-
const numstat = runGitChecked(worktree.path, ["diff", "--cached", "--numstat", setup.baseCommit]);
|
|
1070
|
+
const diffStat = runGitChecked(worktree.path, ["diff", "--cached", ...MACHINE_DIFF_OPTIONS, "--stat", setup.baseCommit]).trim();
|
|
1071
|
+
const patch = runGitChecked(worktree.path, ["diff", "--cached", ...MACHINE_PATCH_OPTIONS, setup.baseCommit]);
|
|
1072
|
+
const numstat = runGitChecked(worktree.path, ["diff", "--cached", ...MACHINE_DIFF_OPTIONS, "--numstat", setup.baseCommit]);
|
|
878
1073
|
fs.writeFileSync(patchPath, patch, "utf-8");
|
|
879
1074
|
|
|
880
1075
|
if (!patch.trim()) {
|
|
881
1076
|
return emptyDiff(worktree.index, agent, worktree.branch, patchPath);
|
|
882
1077
|
}
|
|
883
1078
|
|
|
1079
|
+
const validationError = validateWorktreePatch(worktree.path, patchPath);
|
|
1080
|
+
if (validationError) throw new Error(`captured worktree patch is not machine-applyable: ${validationError}`);
|
|
1081
|
+
|
|
884
1082
|
const parsed = parseNumstat(numstat);
|
|
885
1083
|
return {
|
|
886
1084
|
index: worktree.index,
|
|
@@ -949,7 +1147,7 @@ function cleanupSingleWorktree(
|
|
|
949
1147
|
errors.push(`synthetic path cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
950
1148
|
}
|
|
951
1149
|
const status = runGit(worktree.path, ["status", "--porcelain"]);
|
|
952
|
-
const baseDiff = runGit(worktree.path, ["diff", "--quiet", setup.baseCommit, "--"]);
|
|
1150
|
+
const baseDiff = runGit(worktree.path, ["diff", "--quiet", ...MACHINE_DIFF_OPTIONS, setup.baseCommit, "--"]);
|
|
953
1151
|
if (status.status !== 0 || (baseDiff.status !== 0 && baseDiff.status !== 1)) {
|
|
954
1152
|
const reason = status.status !== 0
|
|
955
1153
|
? status.stderr.trim() || status.stdout.trim() || "git status failed"
|
|
@@ -970,13 +1168,25 @@ function cleanupSingleWorktree(
|
|
|
970
1168
|
const hasWork = status.stdout.trim().length > 0 || baseDiff.status === 1;
|
|
971
1169
|
if (hasWork && intent.kind === "preserve") {
|
|
972
1170
|
const captured = (intent.capturedDiffs ?? setup.capturedDiffs)?.find((diff) => diff.index === worktree.index);
|
|
973
|
-
|
|
1171
|
+
let patchValidationError: string | undefined;
|
|
1172
|
+
let patchCaptured = false;
|
|
1173
|
+
if (captured !== undefined
|
|
974
1174
|
&& captured.error === undefined
|
|
975
1175
|
&& fs.existsSync(captured.patchPath)
|
|
976
|
-
&&
|
|
977
|
-
|
|
1176
|
+
&& handoffRecordsPatch(intent.handoffManifestPath, captured.patchPath)) {
|
|
1177
|
+
try {
|
|
1178
|
+
if (fs.statSync(captured.patchPath).size > 0) {
|
|
1179
|
+
patchValidationError = validateWorktreePatchRepresentsCurrentWorktree(worktree.path, setup.baseCommit, captured.patchPath);
|
|
1180
|
+
patchCaptured = patchValidationError === undefined;
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
patchValidationError = error instanceof Error ? error.message : String(error);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
978
1186
|
if (!patchCaptured) {
|
|
979
|
-
const reason =
|
|
1187
|
+
const reason = patchValidationError
|
|
1188
|
+
? `captured handoff patch failed validation: ${patchValidationError}`
|
|
1189
|
+
: "worktree contains changes that are not represented by a captured handoff patch";
|
|
980
1190
|
return {
|
|
981
1191
|
index: worktree.index,
|
|
982
1192
|
path: worktree.path,
|
|
@@ -1043,54 +1253,111 @@ function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
|
1043
1253
|
return diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0;
|
|
1044
1254
|
}
|
|
1045
1255
|
|
|
1046
|
-
|
|
1256
|
+
async function compensateSetup(tx: SetupTransaction): Promise<WorktreeCleanupReport> {
|
|
1257
|
+
const { setup, attempts } = tx.progress;
|
|
1258
|
+
const report: WorktreeCleanupReport = { state: "partial", tasks: [], pruned: false, errors: [] };
|
|
1259
|
+
tx.progress.cleanup = report;
|
|
1260
|
+
tx.progress.phase = "rollback";
|
|
1261
|
+
// Cancellation stops allocation, not accounting. Compensation has ONLY the
|
|
1262
|
+
// remaining original absolute deadline; it does not reuse the launch signal.
|
|
1263
|
+
const git = async (args: string[], acceptedExitCodes: readonly number[] = [0]) => {
|
|
1264
|
+
if (setupPoison) throw new Error(setupPoison);
|
|
1265
|
+
tx.progress.command = { command: "git", args: ["-C", setup.cwd, ...args] };
|
|
1266
|
+
const result = await runSetupCommand("git", tx.progress.command.args, {
|
|
1267
|
+
deadlineAt: tx.options.deadlineAt, acceptedExitCodes,
|
|
1268
|
+
onSpawn: (process) => { Object.assign(tx.progress.command!, process); tx.publish(); },
|
|
1269
|
+
});
|
|
1270
|
+
const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
|
|
1271
|
+
tx.progress.command.result = metadata;
|
|
1272
|
+
if (result.processTree?.state === "unknown") tx.unknown(result.error ?? "Rollback command settlement unverified");
|
|
1273
|
+
tx.publish();
|
|
1274
|
+
if (result.error || result.status === null || !acceptedExitCodes.includes(result.status)) throw result.error ?? new Error(result.stderr.trim() || "Rollback command failed");
|
|
1275
|
+
return result;
|
|
1276
|
+
};
|
|
1277
|
+
for (const attempt of [...attempts].reverse()) {
|
|
1278
|
+
if (!attempt.path) {
|
|
1279
|
+
tx.unknown(`Allocation path unconfirmed for branch ${attempt.branch}`);
|
|
1280
|
+
report.errors!.push(tx.progress.unknown!);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const known = setup.worktrees.find((worktree) => worktree.index === attempt.index);
|
|
1284
|
+
const task: WorktreeCleanupTask = { index: attempt.index, path: attempt.path, branch: attempt.branch,
|
|
1285
|
+
provider: known?.provider ?? "native", naming: known?.naming, worktreeRemoved: false, branchRemoved: false };
|
|
1286
|
+
report.tasks.push(task);
|
|
1287
|
+
try {
|
|
1288
|
+
if (normalizeComparableCwd(attempt.path) === normalizeComparableCwd(setup.cwd)) throw new Error("Refusing source-checkout removal");
|
|
1289
|
+
if (attempt.validated) {
|
|
1290
|
+
await git(["worktree", "remove", "--force", attempt.path]);
|
|
1291
|
+
task.worktreeRemoved = true;
|
|
1292
|
+
} else {
|
|
1293
|
+
// Native attempts were absent before mutation. Reconcile only exact
|
|
1294
|
+
// registered path/branch identity; never recursively delete a guessed leaf.
|
|
1295
|
+
const listed = (await git(["worktree", "list", "--porcelain"])).stdout;
|
|
1296
|
+
const entry = listed.split("\n\n").find((block) => block.split("\n").includes(`worktree ${attempt.path}`));
|
|
1297
|
+
if (entry) {
|
|
1298
|
+
if (!entry.split("\n").includes(`branch refs/heads/${attempt.branch}`)) {
|
|
1299
|
+
tx.unknown("Partial allocation ownership mismatch");
|
|
1300
|
+
throw new Error(tx.progress.unknown);
|
|
1301
|
+
}
|
|
1302
|
+
await git(["worktree", "remove", "--force", attempt.path]);
|
|
1303
|
+
} else if (fs.existsSync(attempt.path)) {
|
|
1304
|
+
tx.unknown("Unregistered partial allocation retained");
|
|
1305
|
+
throw new Error(tx.progress.unknown);
|
|
1306
|
+
}
|
|
1307
|
+
task.worktreeRemoved = true;
|
|
1308
|
+
}
|
|
1309
|
+
const branch = await git(["show-ref", "--verify", "--quiet", `refs/heads/${attempt.branch}`], [0, 1]);
|
|
1310
|
+
if (branch.status === 0) await git(["branch", "-D", attempt.branch]);
|
|
1311
|
+
task.branchRemoved = true;
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
task.preserved = true;
|
|
1314
|
+
task.errors = [error instanceof Error ? error.message : String(error)];
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
try {
|
|
1318
|
+
if (attempts.length > 0) { await git(["worktree", "prune"]); report.pruned = true; }
|
|
1319
|
+
} catch (error) { report.errors!.push(error instanceof Error ? error.message : String(error)); }
|
|
1320
|
+
if (tx.progress.unknown) report.errors!.push(tx.progress.unknown);
|
|
1321
|
+
report.tasks.sort((a, b) => a.index - b.index);
|
|
1322
|
+
if (!setupPoison && (report.pruned || attempts.length === 0) && report.tasks.every((task) => task.worktreeRemoved && task.branchRemoved) && !report.errors!.length) report.state = "complete";
|
|
1323
|
+
return report;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async function allocateWorktrees(tx: SetupTransaction, cwd: string, runId: string, count: number): Promise<WorktreeSetup> {
|
|
1327
|
+
const options = tx.options;
|
|
1328
|
+
tx.check();
|
|
1047
1329
|
if (!Number.isSafeInteger(count) || count < 0) throw new Error("worktree count must be a non-negative integer");
|
|
1048
|
-
const repo = resolveRepoState(cwd);
|
|
1330
|
+
const repo = await resolveRepoState(tx, cwd, options?.baseRef);
|
|
1331
|
+
tx.progress.setup.cwd = repo.toplevel;
|
|
1332
|
+
tx.progress.setup.baseCommit = repo.baseCommit;
|
|
1049
1333
|
const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
|
|
1050
|
-
|
|
1334
|
+
const provider = await resolveSetupProvider(tx, options?.provider, options?.baseDir);
|
|
1051
1335
|
const branchPrefix = normalizeWorktreeBranchPrefix(options?.branchPrefix);
|
|
1052
|
-
|
|
1053
|
-
const worktrees
|
|
1336
|
+
const dedicatedRoot = provider === "native" ? resolveWorktreeDedicatedRoot(options?.baseDir, repo.toplevel) : undefined;
|
|
1337
|
+
const worktrees = tx.progress.setup.worktrees;
|
|
1054
1338
|
|
|
1055
1339
|
try {
|
|
1056
1340
|
if (provider === "native") {
|
|
1057
|
-
const plannedSetup: WorktreeSetup = {
|
|
1058
|
-
cwd: repo.toplevel,
|
|
1059
|
-
baseCommit: repo.baseCommit,
|
|
1060
|
-
worktrees: Array.from({ length: count }, (_, index) => {
|
|
1061
|
-
const naming = buildWorktreeNaming({ runId, index, agent: options?.agents?.[index], label: options?.labels?.[index], task: options?.tasks?.[index], branchPrefix });
|
|
1062
|
-
const worktreePath = buildNativeWorktreePath(baseDir!, runId, index);
|
|
1063
|
-
return {
|
|
1064
|
-
path: worktreePath,
|
|
1065
|
-
agentCwd: repo.cwdRelative ? path.join(worktreePath, repo.cwdRelative) : worktreePath,
|
|
1066
|
-
branch: naming.requestedBranch,
|
|
1067
|
-
index,
|
|
1068
|
-
nodeModulesLinked: false,
|
|
1069
|
-
syntheticPaths: [],
|
|
1070
|
-
provider: "native",
|
|
1071
|
-
naming,
|
|
1072
|
-
};
|
|
1073
|
-
}),
|
|
1074
|
-
};
|
|
1075
|
-
options?.beforeCreate?.(plannedSetup);
|
|
1076
1341
|
for (let index = 0; index < count; index++) {
|
|
1077
|
-
worktrees
|
|
1078
|
-
|
|
1342
|
+
worktrees[index] = await createNativeWorktree(
|
|
1343
|
+
tx,
|
|
1344
|
+
repo.toplevel,
|
|
1079
1345
|
repo.cwdRelative,
|
|
1080
1346
|
runId,
|
|
1081
1347
|
index,
|
|
1082
1348
|
repo.baseCommit,
|
|
1083
1349
|
setupHook,
|
|
1084
1350
|
options?.agents?.[index],
|
|
1085
|
-
|
|
1351
|
+
dedicatedRoot!,
|
|
1086
1352
|
options?.labels,
|
|
1087
1353
|
options?.tasks,
|
|
1088
1354
|
branchPrefix,
|
|
1089
|
-
)
|
|
1355
|
+
);
|
|
1090
1356
|
}
|
|
1091
1357
|
} else {
|
|
1092
1358
|
for (let index = 0; index < count; index++) {
|
|
1093
|
-
|
|
1359
|
+
await createWorktrunkWorktree(
|
|
1360
|
+
tx,
|
|
1094
1361
|
repo.toplevel,
|
|
1095
1362
|
repo.cwdRelative,
|
|
1096
1363
|
runId,
|
|
@@ -1100,13 +1367,11 @@ export function createWorktrees(cwd: string, runId: string, count: number, optio
|
|
|
1100
1367
|
options?.labels,
|
|
1101
1368
|
options?.tasks,
|
|
1102
1369
|
branchPrefix,
|
|
1103
|
-
)
|
|
1370
|
+
);
|
|
1104
1371
|
}
|
|
1105
|
-
// Worktrunk determines its path only after creation. Journal the exact
|
|
1106
|
-
// returned ownership before Pi runs setup hooks or launches a child.
|
|
1107
|
-
options?.beforeCreate?.({ cwd: repo.toplevel, baseCommit: repo.baseCommit, worktrees: [...worktrees] });
|
|
1108
1372
|
for (let index = 0; index < worktrees.length; index++) {
|
|
1109
|
-
worktrees[index] = finalizeCreatedWorktree(
|
|
1373
|
+
worktrees[index] = await finalizeCreatedWorktree(
|
|
1374
|
+
tx,
|
|
1110
1375
|
repo.toplevel,
|
|
1111
1376
|
repo.cwdRelative,
|
|
1112
1377
|
runId,
|
|
@@ -1117,23 +1382,37 @@ export function createWorktrees(cwd: string, runId: string, count: number, optio
|
|
|
1117
1382
|
);
|
|
1118
1383
|
}
|
|
1119
1384
|
}
|
|
1385
|
+
tx.check();
|
|
1386
|
+
tx.progress.phase = "ready";
|
|
1387
|
+
tx.publish();
|
|
1388
|
+
tx.check();
|
|
1389
|
+
return tx.progress.setup;
|
|
1120
1390
|
} catch (error) {
|
|
1121
|
-
|
|
1122
|
-
cwd: repo.toplevel,
|
|
1123
|
-
worktrees,
|
|
1124
|
-
baseCommit: repo.baseCommit,
|
|
1125
|
-
}, { kind: "setup-rollback" });
|
|
1391
|
+
await compensateSetup(tx);
|
|
1126
1392
|
throw error;
|
|
1127
1393
|
}
|
|
1394
|
+
}
|
|
1128
1395
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1396
|
+
export async function createWorktrees(cwd: string, runId: string, count: number, options: CreateWorktreesOptions = {}): Promise<WorktreeSetup> {
|
|
1397
|
+
const tx = new SetupTransaction(cwd, options);
|
|
1398
|
+
try {
|
|
1399
|
+
return await withWorktreeTransaction(async () => {
|
|
1400
|
+
setupActive = true;
|
|
1401
|
+
try { return await allocateWorktrees(tx, cwd, runId, count); }
|
|
1402
|
+
finally { setupActive = false; }
|
|
1403
|
+
});
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
if (setupPoison) tx.progress.unknown ??= setupPoison;
|
|
1406
|
+
tx.progress.cleanup ??= { state: tx.progress.unknown ? "partial" : "complete", tasks: [], pruned: false, errors: tx.progress.unknown ? [tx.progress.unknown] : [] };
|
|
1407
|
+
try { tx.publish(); } catch (publicationError) {
|
|
1408
|
+
tx.progress.cleanup?.errors?.push(`Recovery publication failed: ${String(publicationError)}`);
|
|
1409
|
+
}
|
|
1410
|
+
throw new WorktreeSetupError(error, tx.snapshot());
|
|
1411
|
+
}
|
|
1134
1412
|
}
|
|
1135
1413
|
|
|
1136
1414
|
export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir: string): WorktreeDiff[] {
|
|
1415
|
+
assertWorktreeMutationAllowed();
|
|
1137
1416
|
try {
|
|
1138
1417
|
fs.mkdirSync(diffsDir, { recursive: true });
|
|
1139
1418
|
} catch {
|
|
@@ -1163,6 +1442,7 @@ export function cleanupWorktrees(
|
|
|
1163
1442
|
setup: WorktreeSetup,
|
|
1164
1443
|
intent: WorktreeCleanupIntent = { kind: "preserve", ...(setup.capturedDiffs ? { capturedDiffs: setup.capturedDiffs } : {}) },
|
|
1165
1444
|
): WorktreeCleanupReport {
|
|
1445
|
+
assertWorktreeMutationAllowed();
|
|
1166
1446
|
const tasks: WorktreeCleanupTask[] = [];
|
|
1167
1447
|
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
1168
1448
|
tasks.push(cleanupSingleWorktree(setup, setup.worktrees[index]!, intent));
|