@fitchmultz/pi-subagents 0.0.0-stage → 0.44.4
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 +1645 -0
- package/LICENSE +21 -0
- package/README.md +1529 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +194 -0
- package/dist/history/ingest.js +447 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +461 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +77 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +139 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +103 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +320 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom.md +579 -0
- package/docs/performance.md +121 -0
- package/package.json +101 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +205 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +152 -0
- package/src/history/ingest.ts +332 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +317 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +70 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +121 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +93 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +372 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1259 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { addAbortListener } from "node:events";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { TEMP_ROOT_DIR } from "../../shared/types.ts";
|
|
7
|
+
import { ensureTempRoot } from "../../shared/temp-root.ts";
|
|
8
|
+
import { trySignalChildTree } from "../../shared/post-exit-stdio-guard.ts";
|
|
9
|
+
|
|
10
|
+
export class WorktreeCleanupError extends Error {}
|
|
11
|
+
|
|
12
|
+
export interface WorktreeSetup {
|
|
13
|
+
cwd: string;
|
|
14
|
+
worktrees: WorktreeInfo[];
|
|
15
|
+
baseCommit: string;
|
|
16
|
+
preserveOnCleanup?: boolean;
|
|
17
|
+
preservationReason?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface WorktreeInfo {
|
|
21
|
+
path: string;
|
|
22
|
+
agentCwd: string;
|
|
23
|
+
branch: string;
|
|
24
|
+
index: number;
|
|
25
|
+
syntheticPaths: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface WorktreeDiff {
|
|
29
|
+
index: number;
|
|
30
|
+
agent: string;
|
|
31
|
+
branch: string;
|
|
32
|
+
diffStat: string;
|
|
33
|
+
filesChanged: number;
|
|
34
|
+
insertions: number;
|
|
35
|
+
deletions: number;
|
|
36
|
+
patchPath: string;
|
|
37
|
+
worktreePath?: string;
|
|
38
|
+
captureError?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface WorktreeTaskCwdConflict {
|
|
42
|
+
index: number;
|
|
43
|
+
agent: string;
|
|
44
|
+
cwd: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface WorktreeSetupHookConfig {
|
|
48
|
+
hookPath: string;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CreateWorktreesOptions {
|
|
53
|
+
agents?: string[];
|
|
54
|
+
setupHook?: WorktreeSetupHookConfig;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ResolvedWorktreeSetupHook {
|
|
59
|
+
hookPath: string;
|
|
60
|
+
timeoutMs: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface WorktreeSetupHookInput {
|
|
64
|
+
version: 1;
|
|
65
|
+
repoRoot: string;
|
|
66
|
+
worktreePath: string;
|
|
67
|
+
agentCwd: string;
|
|
68
|
+
branch: string;
|
|
69
|
+
index: number;
|
|
70
|
+
runId: string;
|
|
71
|
+
baseCommit: string;
|
|
72
|
+
agent?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface WorktreeSetupHookOutput {
|
|
76
|
+
syntheticPaths?: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface GitResult {
|
|
80
|
+
stdout: string;
|
|
81
|
+
stderr: string;
|
|
82
|
+
status: number | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface RepoState {
|
|
86
|
+
toplevel: string;
|
|
87
|
+
cwdRelative: string;
|
|
88
|
+
baseCommit: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
|
|
92
|
+
|
|
93
|
+
function runGit(cwd: string, args: string[]): GitResult {
|
|
94
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
95
|
+
return {
|
|
96
|
+
stdout: result.stdout ?? "",
|
|
97
|
+
stderr: result.stderr ?? "",
|
|
98
|
+
status: result.status,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runGitChecked(cwd: string, args: string[]): string {
|
|
103
|
+
const result = runGit(cwd, args);
|
|
104
|
+
if (result.status !== 0) {
|
|
105
|
+
const command = `git -C ${cwd} ${args.join(" ")}`;
|
|
106
|
+
const message = result.stderr.trim() || result.stdout.trim() || `${command} failed`;
|
|
107
|
+
throw new Error(message);
|
|
108
|
+
}
|
|
109
|
+
return result.stdout;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function runSetupCommand(
|
|
113
|
+
command: string, args: string[], cwd: string, signal?: AbortSignal, input?: string, timeoutMs?: number,
|
|
114
|
+
): Promise<GitResult> {
|
|
115
|
+
signal?.throwIfAborted();
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
let stdout = "";
|
|
118
|
+
let stderr = "";
|
|
119
|
+
let outputBytes = 0;
|
|
120
|
+
let failure: Error | undefined;
|
|
121
|
+
const child = spawn(command, args, { cwd, detached: true, stdio: "pipe" });
|
|
122
|
+
const terminate = () => { trySignalChildTree(child, "SIGKILL"); };
|
|
123
|
+
const abortListener = signal && addAbortListener(signal, terminate);
|
|
124
|
+
const timer = timeoutMs === undefined ? undefined : setTimeout(() => {
|
|
125
|
+
failure = new Error(`worktree setup hook timed out after ${timeoutMs}ms`);
|
|
126
|
+
terminate();
|
|
127
|
+
}, timeoutMs);
|
|
128
|
+
timer?.unref();
|
|
129
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
130
|
+
stream.setEncoding("utf8");
|
|
131
|
+
stream.on("data", (chunk: string) => {
|
|
132
|
+
outputBytes += Buffer.byteLength(chunk);
|
|
133
|
+
// Keep the previous spawnSync output bound.
|
|
134
|
+
if (outputBytes > 1024 * 1024) {
|
|
135
|
+
failure ??= new Error(`${command} output exceeded 1048576 bytes`);
|
|
136
|
+
terminate();
|
|
137
|
+
} else if (stream === child.stdout) stdout += chunk;
|
|
138
|
+
else stderr += chunk;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
child.on("error", (error) => { failure = error; terminate(); });
|
|
142
|
+
child.on("exit", (status) => { if (status !== 0) terminate(); });
|
|
143
|
+
child.on("close", (status) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
abortListener?.[Symbol.dispose]();
|
|
146
|
+
if (signal?.aborted) reject(signal.reason);
|
|
147
|
+
else if (failure) reject(failure);
|
|
148
|
+
else resolve({ stdout, stderr, status });
|
|
149
|
+
});
|
|
150
|
+
child.stdin.on("error", (error: NodeJS.ErrnoException) => {
|
|
151
|
+
if (error.code === "EPIPE") return; // The hook can exit without reading its payload.
|
|
152
|
+
failure = error;
|
|
153
|
+
terminate();
|
|
154
|
+
});
|
|
155
|
+
child.stdin.end(input);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function runSetupGit(cwd: string, args: string[], signal?: AbortSignal): Promise<string> {
|
|
160
|
+
const result = await runSetupCommand("git", ["-C", cwd, ...args], cwd, signal);
|
|
161
|
+
if (result.status !== 0) {
|
|
162
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || `git -C ${cwd} ${args.join(" ")} failed`);
|
|
163
|
+
}
|
|
164
|
+
return result.stdout;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function resolveRepoState(cwd: string, signal?: AbortSignal): Promise<RepoState> {
|
|
168
|
+
const repoCheck = await runSetupCommand("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], cwd, signal);
|
|
169
|
+
if (repoCheck.status !== 0 || repoCheck.stdout.trim() !== "true") {
|
|
170
|
+
throw new Error("worktree isolation requires a git repository");
|
|
171
|
+
}
|
|
172
|
+
const cwdRelative = (await runSetupGit(cwd, ["rev-parse", "--show-prefix"], signal)).trim().replace(/[\\/]+$/, "");
|
|
173
|
+
const toplevel = (await runSetupGit(cwd, ["rev-parse", "--show-toplevel"], signal)).trim();
|
|
174
|
+
|
|
175
|
+
const status = await runSetupGit(toplevel, ["status", "--porcelain"], signal);
|
|
176
|
+
if (status.trim().length > 0) {
|
|
177
|
+
throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first, or rerun without worktree isolation if shared-checkout edits are intentional.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const baseCommit = (await runSetupGit(toplevel, ["rev-parse", "HEAD"], signal)).trim();
|
|
181
|
+
return { toplevel, cwdRelative, baseCommit };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeComparableCwd(cwd: string): string {
|
|
185
|
+
const resolved = path.resolve(cwd);
|
|
186
|
+
try {
|
|
187
|
+
return fs.realpathSync(resolved);
|
|
188
|
+
} catch {
|
|
189
|
+
// Use the unresolved absolute path when realpath resolution is unavailable.
|
|
190
|
+
return resolved;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function findWorktreeTaskCwdConflict(
|
|
195
|
+
tasks: ReadonlyArray<{ agent: string; cwd?: string }>,
|
|
196
|
+
sharedCwd: string,
|
|
197
|
+
): WorktreeTaskCwdConflict | undefined {
|
|
198
|
+
const normalizedSharedCwd = normalizeComparableCwd(sharedCwd);
|
|
199
|
+
for (let index = 0; index < tasks.length; index++) {
|
|
200
|
+
const task = tasks[index]!;
|
|
201
|
+
if (!task.cwd) continue;
|
|
202
|
+
const taskCwd = path.isAbsolute(task.cwd) ? task.cwd : path.resolve(sharedCwd, task.cwd);
|
|
203
|
+
if (normalizeComparableCwd(taskCwd) === normalizedSharedCwd) continue;
|
|
204
|
+
return { index, agent: task.agent, cwd: task.cwd };
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function formatWorktreeTaskCwdConflict(
|
|
210
|
+
conflict: WorktreeTaskCwdConflict,
|
|
211
|
+
sharedCwd: string,
|
|
212
|
+
): string {
|
|
213
|
+
return `worktree isolation uses the shared cwd (${sharedCwd}); task ${conflict.index + 1} (${conflict.agent}) sets cwd to ${conflict.cwd}. Remove task-level cwd overrides or disable worktree.`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function safePatchAgentName(agent: string): string {
|
|
217
|
+
return agent.replace(/[^\w.-]/g, "_");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildWorktreeBranch(runId: string, index: number): string {
|
|
221
|
+
return `pi-parallel-${runId}-${index}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildWorktreePath(runId: string, index: number): string {
|
|
225
|
+
return path.join(TEMP_ROOT_DIR, "worktrees", `pi-worktree-${runId}-${index}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolveRepoCwdRelative(cwd: string): string {
|
|
229
|
+
const repoCheck = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
230
|
+
if (repoCheck.status !== 0 || repoCheck.stdout.trim() !== "true") {
|
|
231
|
+
throw new Error("worktree isolation requires a git repository");
|
|
232
|
+
}
|
|
233
|
+
const rawPrefix = runGitChecked(cwd, ["rev-parse", "--show-prefix"]).trim();
|
|
234
|
+
const normalizedPrefix = rawPrefix
|
|
235
|
+
? path.normalize(rawPrefix.replace(/[\\/]+$/, ""))
|
|
236
|
+
: "";
|
|
237
|
+
return normalizedPrefix === "." ? "" : normalizedPrefix;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function resolveExpectedWorktreeAgentCwd(cwd: string, runId: string, index: number): string {
|
|
241
|
+
const cwdRelative = resolveRepoCwdRelative(cwd);
|
|
242
|
+
const worktreePath = buildWorktreePath(runId, index);
|
|
243
|
+
return cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseHookTimeout(timeoutMs: number | undefined): number {
|
|
247
|
+
if (timeoutMs === undefined) return DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS;
|
|
248
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
249
|
+
throw new Error("worktree setup hook timeout must be an integer greater than 0");
|
|
250
|
+
}
|
|
251
|
+
return timeoutMs;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveWorktreeSetupHook(
|
|
255
|
+
repoRoot: string,
|
|
256
|
+
config: WorktreeSetupHookConfig | undefined,
|
|
257
|
+
): ResolvedWorktreeSetupHook | undefined {
|
|
258
|
+
if (!config) return undefined;
|
|
259
|
+
const hookPath = config.hookPath.trim();
|
|
260
|
+
if (!hookPath) {
|
|
261
|
+
throw new Error("worktree setup hook path cannot be empty");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const expandedHookPath = hookPath.startsWith("~/") ? path.join(os.homedir(), hookPath.slice(2)) : hookPath;
|
|
265
|
+
let resolvedPath: string;
|
|
266
|
+
if (path.isAbsolute(expandedHookPath)) {
|
|
267
|
+
resolvedPath = expandedHookPath;
|
|
268
|
+
} else if (expandedHookPath.includes("/") || expandedHookPath.includes("\\")) {
|
|
269
|
+
resolvedPath = path.resolve(repoRoot, expandedHookPath);
|
|
270
|
+
} else {
|
|
271
|
+
throw new Error("worktree setup hook must be an absolute path or a repo-relative path");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
275
|
+
throw new Error(`worktree setup hook not found: ${resolvedPath}`);
|
|
276
|
+
}
|
|
277
|
+
if (fs.statSync(resolvedPath).isDirectory()) {
|
|
278
|
+
throw new Error(`worktree setup hook must be a file, got directory: ${resolvedPath}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
hookPath: resolvedPath,
|
|
283
|
+
timeoutMs: parseHookTimeout(config.timeoutMs),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
|
|
288
|
+
const trimmed = rawPath.trim();
|
|
289
|
+
if (!trimmed) throw new Error("synthetic path cannot be empty");
|
|
290
|
+
if (path.isAbsolute(trimmed)) throw new Error(`synthetic path must be relative: ${rawPath}`);
|
|
291
|
+
|
|
292
|
+
const resolved = path.resolve(worktreePath, trimmed);
|
|
293
|
+
const relative = path.relative(worktreePath, resolved);
|
|
294
|
+
if (!relative || relative === ".") {
|
|
295
|
+
throw new Error(`synthetic path cannot target the worktree root: ${rawPath}`);
|
|
296
|
+
}
|
|
297
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
298
|
+
throw new Error(`synthetic path escapes the worktree root: ${rawPath}`);
|
|
299
|
+
}
|
|
300
|
+
return path.normalize(relative);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function hasTrackedEntries(worktreePath: string, relativePath: string, signal?: AbortSignal): Promise<boolean> {
|
|
304
|
+
const result = await runSetupCommand("git", ["-C", worktreePath, "ls-files", "--", relativePath], worktreePath, signal);
|
|
305
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseWorktreeSetupHookOutput(rawStdout: string): WorktreeSetupHookOutput {
|
|
309
|
+
const trimmed = rawStdout.trim();
|
|
310
|
+
if (!trimmed) {
|
|
311
|
+
throw new Error("worktree setup hook returned empty stdout; expected JSON object");
|
|
312
|
+
}
|
|
313
|
+
let parsed: unknown;
|
|
314
|
+
try {
|
|
315
|
+
parsed = JSON.parse(trimmed);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
throw new Error(`worktree setup hook returned invalid JSON: ${message}`);
|
|
319
|
+
}
|
|
320
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
321
|
+
throw new Error("worktree setup hook stdout must be a JSON object");
|
|
322
|
+
}
|
|
323
|
+
return parsed as WorktreeSetupHookOutput;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function runWorktreeSetupHook(
|
|
327
|
+
hook: ResolvedWorktreeSetupHook,
|
|
328
|
+
input: WorktreeSetupHookInput,
|
|
329
|
+
signal?: AbortSignal,
|
|
330
|
+
): Promise<string[]> {
|
|
331
|
+
let result: GitResult;
|
|
332
|
+
try {
|
|
333
|
+
result = await runSetupCommand(hook.hookPath, [], input.worktreePath, signal, JSON.stringify(input), hook.timeoutMs);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
signal?.throwIfAborted();
|
|
336
|
+
throw new Error(`worktree setup hook failed: ${getErrorMessage(error)}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (result.status !== 0) {
|
|
340
|
+
const details = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
341
|
+
throw new Error(`worktree setup hook failed with exit code ${result.status}: ${details}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const output = parseWorktreeSetupHookOutput(result.stdout);
|
|
345
|
+
if (output.syntheticPaths === undefined) return [];
|
|
346
|
+
if (!Array.isArray(output.syntheticPaths)) {
|
|
347
|
+
throw new Error("worktree setup hook output field 'syntheticPaths' must be an array of relative paths");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const uniquePaths = new Set<string>();
|
|
351
|
+
for (const candidate of output.syntheticPaths) {
|
|
352
|
+
if (typeof candidate !== "string") {
|
|
353
|
+
throw new Error("worktree setup hook output field 'syntheticPaths' must contain only strings");
|
|
354
|
+
}
|
|
355
|
+
const normalizedPath = normalizeSyntheticPath(input.worktreePath, candidate);
|
|
356
|
+
if (await hasTrackedEntries(input.worktreePath, normalizedPath, signal)) {
|
|
357
|
+
throw new Error(`worktree setup hook cannot mark tracked paths as synthetic: ${normalizedPath}`);
|
|
358
|
+
}
|
|
359
|
+
uniquePaths.add(normalizedPath);
|
|
360
|
+
}
|
|
361
|
+
return [...uniquePaths];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function removeSyntheticPath(worktree: WorktreeInfo, syntheticPath: string): void {
|
|
365
|
+
const resolved = path.resolve(worktree.path, syntheticPath);
|
|
366
|
+
const relative = path.relative(worktree.path, resolved);
|
|
367
|
+
if (!relative || relative === "." || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let stat: fs.Stats;
|
|
372
|
+
try {
|
|
373
|
+
stat = fs.lstatSync(resolved);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
376
|
+
if (code === "ENOENT") return;
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const realRoot = fs.realpathSync(worktree.path);
|
|
381
|
+
const realParent = fs.realpathSync(path.dirname(resolved));
|
|
382
|
+
const parentRelative = path.relative(realRoot, realParent);
|
|
383
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path.sep}`) || path.isAbsolute(parentRelative)) {
|
|
384
|
+
throw new Error(`synthetic path resolves outside the worktree: ${syntheticPath}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (stat.isSymbolicLink()) {
|
|
388
|
+
fs.unlinkSync(resolved);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (stat.isDirectory()) {
|
|
392
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
fs.rmSync(resolved, { force: true });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function removeSyntheticPathsBeforeDiff(worktree: WorktreeInfo): void {
|
|
399
|
+
if (worktree.syntheticPaths.length === 0) return;
|
|
400
|
+
const seen = new Set<string>();
|
|
401
|
+
for (const syntheticPath of worktree.syntheticPaths) {
|
|
402
|
+
if (seen.has(syntheticPath)) continue;
|
|
403
|
+
seen.add(syntheticPath);
|
|
404
|
+
removeSyntheticPath(worktree, syntheticPath);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function emptyDiff(index: number, agent: string, branch: string, patchPath: string, worktreePath?: string, captureError?: string): WorktreeDiff {
|
|
409
|
+
return {
|
|
410
|
+
index,
|
|
411
|
+
agent,
|
|
412
|
+
branch,
|
|
413
|
+
diffStat: "",
|
|
414
|
+
filesChanged: 0,
|
|
415
|
+
insertions: 0,
|
|
416
|
+
deletions: 0,
|
|
417
|
+
patchPath,
|
|
418
|
+
...(worktreePath ? { worktreePath } : {}),
|
|
419
|
+
...(captureError ? { captureError } : {}),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseNumstat(numstat: string): { filesChanged: number; insertions: number; deletions: number } {
|
|
424
|
+
const lines = numstat
|
|
425
|
+
.split("\n")
|
|
426
|
+
.map((line) => line.trim())
|
|
427
|
+
.filter(Boolean);
|
|
428
|
+
let filesChanged = 0;
|
|
429
|
+
let insertions = 0;
|
|
430
|
+
let deletions = 0;
|
|
431
|
+
|
|
432
|
+
for (const line of lines) {
|
|
433
|
+
const [rawInsertions, rawDeletions] = line.split("\t");
|
|
434
|
+
if (rawInsertions === undefined || rawDeletions === undefined) continue;
|
|
435
|
+
filesChanged++;
|
|
436
|
+
if (/^\d+$/.test(rawInsertions)) insertions += parseInt(rawInsertions, 10);
|
|
437
|
+
if (/^\d+$/.test(rawDeletions)) deletions += parseInt(rawDeletions, 10);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { filesChanged, insertions, deletions };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function captureWorktreeDiff(
|
|
444
|
+
setup: WorktreeSetup,
|
|
445
|
+
worktree: WorktreeInfo,
|
|
446
|
+
agent: string,
|
|
447
|
+
patchPath: string,
|
|
448
|
+
): WorktreeDiff {
|
|
449
|
+
removeSyntheticPathsBeforeDiff(worktree);
|
|
450
|
+
runGitChecked(worktree.path, ["add", "-A"]);
|
|
451
|
+
const diffStat = runGitChecked(worktree.path, ["diff", "--cached", "--stat", setup.baseCommit]).trim();
|
|
452
|
+
const patch = runGitChecked(worktree.path, ["diff", "--cached", "--binary", "--no-textconv", "--no-ext-diff", setup.baseCommit]);
|
|
453
|
+
const numstat = runGitChecked(worktree.path, ["diff", "--cached", "--numstat", setup.baseCommit]);
|
|
454
|
+
fs.writeFileSync(patchPath, patch, "utf-8");
|
|
455
|
+
|
|
456
|
+
if (!patch.trim()) {
|
|
457
|
+
return emptyDiff(worktree.index, agent, worktree.branch, patchPath, worktree.path);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const parsed = parseNumstat(numstat);
|
|
461
|
+
return {
|
|
462
|
+
index: worktree.index,
|
|
463
|
+
agent,
|
|
464
|
+
branch: worktree.branch,
|
|
465
|
+
diffStat,
|
|
466
|
+
filesChanged: parsed.filesChanged,
|
|
467
|
+
insertions: parsed.insertions,
|
|
468
|
+
deletions: parsed.deletions,
|
|
469
|
+
patchPath,
|
|
470
|
+
worktreePath: worktree.path,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function writeEmptyPatch(patchPath: string): void {
|
|
475
|
+
try {
|
|
476
|
+
fs.writeFileSync(patchPath, "", "utf-8");
|
|
477
|
+
} catch {
|
|
478
|
+
// Diff artifact writing is best-effort in error paths.
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
|
|
483
|
+
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch (error) {
|
|
484
|
+
console.error(`Failed to remove worktree '${worktree.path}': ${getErrorMessage(error)}`);
|
|
485
|
+
}
|
|
486
|
+
try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch (error) {
|
|
487
|
+
console.error(`Failed to delete worktree branch '${worktree.branch}': ${getErrorMessage(error)}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
492
|
+
return Boolean(diff.captureError) || diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function getErrorMessage(error: unknown): string {
|
|
496
|
+
return error instanceof Error ? error.message : String(error);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function markWorktreesForPreservation(setup: WorktreeSetup, reason: string): void {
|
|
500
|
+
setup.preserveOnCleanup = true;
|
|
501
|
+
setup.preservationReason = setup.preservationReason
|
|
502
|
+
? `${setup.preservationReason}; ${reason}`
|
|
503
|
+
: reason;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export async function createWorktrees(cwd: string, runId: string, count: number, options?: CreateWorktreesOptions): Promise<WorktreeSetup> {
|
|
507
|
+
const signal = options?.signal;
|
|
508
|
+
const repo = await resolveRepoState(cwd, signal);
|
|
509
|
+
const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
|
|
510
|
+
const worktrees: WorktreeInfo[] = [];
|
|
511
|
+
const rollback: Array<{ path?: string; branch?: string }> = [];
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
for (let index = 0; index < count; index++) {
|
|
515
|
+
const branch = buildWorktreeBranch(runId, index);
|
|
516
|
+
const worktreePath = buildWorktreePath(runId, index);
|
|
517
|
+
ensureTempRoot();
|
|
518
|
+
const pathExisted = fs.existsSync(worktreePath);
|
|
519
|
+
const branchExisted = (await runSetupGit(repo.toplevel, ["branch", "--list", branch], signal)).trim().length > 0;
|
|
520
|
+
rollback.push({ path: pathExisted ? undefined : worktreePath, branch: branchExisted ? undefined : branch });
|
|
521
|
+
await runSetupGit(repo.toplevel, ["worktree", "add", worktreePath, "-b", branch, "HEAD"], signal);
|
|
522
|
+
const agentCwd = repo.cwdRelative ? path.join(worktreePath, repo.cwdRelative) : worktreePath;
|
|
523
|
+
const syntheticPaths = setupHook ? await runWorktreeSetupHook(setupHook, {
|
|
524
|
+
version: 1, repoRoot: repo.toplevel, worktreePath, agentCwd, branch, index, runId,
|
|
525
|
+
baseCommit: repo.baseCommit, agent: options?.agents?.[index],
|
|
526
|
+
}, signal) : [];
|
|
527
|
+
worktrees.push({ path: worktreePath, agentCwd, branch, index, syntheticPaths });
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
// Rollback still runs after Stop, but its own Git hooks must not hang the owner.
|
|
531
|
+
const cleanupSignal = AbortSignal.timeout(setupHook?.timeoutMs ?? DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS);
|
|
532
|
+
const failures: string[] = [];
|
|
533
|
+
for (const entry of rollback.reverse()) {
|
|
534
|
+
if (entry.path && fs.existsSync(entry.path)) {
|
|
535
|
+
try {
|
|
536
|
+
// Interrupted checkout can leave Git's "initializing" lock on this new worktree.
|
|
537
|
+
await runSetupGit(repo.toplevel, ["worktree", "remove", "--force", "--force", entry.path], cleanupSignal);
|
|
538
|
+
} catch (cleanupError) { failures.push(`Could not remove worktree '${entry.path}': ${getErrorMessage(cleanupError)}`); }
|
|
539
|
+
}
|
|
540
|
+
if (entry.branch) {
|
|
541
|
+
try {
|
|
542
|
+
const exists = (await runSetupGit(repo.toplevel, ["branch", "--list", entry.branch], cleanupSignal)).trim();
|
|
543
|
+
if (exists) await runSetupGit(repo.toplevel, ["branch", "-D", entry.branch], cleanupSignal);
|
|
544
|
+
} catch (cleanupError) { failures.push(`Could not delete worktree branch '${entry.branch}': ${getErrorMessage(cleanupError)}`); }
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (failures.length) throw new WorktreeCleanupError(`${getErrorMessage(error)}\n\nWorktree rollback incomplete:\n${failures.join("\n")}`);
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { cwd: repo.toplevel, worktrees, baseCommit: repo.baseCommit };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir: string): WorktreeDiff[] {
|
|
555
|
+
try {
|
|
556
|
+
fs.mkdirSync(diffsDir, { recursive: true });
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const message = `failed to create worktree diff artifact directory '${diffsDir}': ${getErrorMessage(error)}`;
|
|
559
|
+
markWorktreesForPreservation(setup, message);
|
|
560
|
+
return setup.worktrees.map((worktree, index) => {
|
|
561
|
+
const agent = agents[index] ?? `task-${index + 1}`;
|
|
562
|
+
const patchPath = path.join(diffsDir, `task-${index}-${safePatchAgentName(agent)}.patch`);
|
|
563
|
+
return emptyDiff(index, agent, worktree.branch, patchPath, worktree.path, message);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const diffs: WorktreeDiff[] = [];
|
|
568
|
+
for (let index = 0; index < setup.worktrees.length; index++) {
|
|
569
|
+
const worktree = setup.worktrees[index]!;
|
|
570
|
+
const agent = agents[index] ?? `task-${index + 1}`;
|
|
571
|
+
const patchPath = path.join(diffsDir, `task-${index}-${safePatchAgentName(agent)}.patch`);
|
|
572
|
+
try {
|
|
573
|
+
diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
|
|
574
|
+
} catch (error) {
|
|
575
|
+
const message = `failed to capture worktree diff for task ${index + 1} (${agent}) at '${worktree.path}': ${getErrorMessage(error)}`;
|
|
576
|
+
markWorktreesForPreservation(setup, message);
|
|
577
|
+
writeEmptyPatch(patchPath);
|
|
578
|
+
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath, worktree.path, message));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return diffs;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function cleanupWorktrees(setup: WorktreeSetup): void {
|
|
586
|
+
if (setup.preserveOnCleanup) return;
|
|
587
|
+
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
588
|
+
cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
|
|
589
|
+
}
|
|
590
|
+
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch (error) {
|
|
591
|
+
console.error(`Failed to prune worktrees in '${setup.cwd}': ${getErrorMessage(error)}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|
|
596
|
+
const changed = diffs.filter(hasWorktreeChanges);
|
|
597
|
+
if (changed.length === 0) return "";
|
|
598
|
+
|
|
599
|
+
const lines: string[] = ["=== Worktree Changes ===", ""];
|
|
600
|
+
for (const diff of changed) {
|
|
601
|
+
lines.push(
|
|
602
|
+
`--- Task ${diff.index + 1} (${diff.agent}): ${diff.filesChanged} files changed, +${diff.insertions} -${diff.deletions} ---`,
|
|
603
|
+
);
|
|
604
|
+
if (diff.captureError) {
|
|
605
|
+
lines.push(`Diff capture failed: ${diff.captureError}`);
|
|
606
|
+
if (diff.worktreePath) lines.push(`Preserved worktree: ${diff.worktreePath}`);
|
|
607
|
+
lines.push(`Preserved branch: ${diff.branch}`);
|
|
608
|
+
}
|
|
609
|
+
if (diff.diffStat.trim().length > 0) {
|
|
610
|
+
lines.push(diff.diffStat);
|
|
611
|
+
}
|
|
612
|
+
lines.push("");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const patchesDir = path.dirname(changed[0]!.patchPath);
|
|
616
|
+
if (changed.some((diff) => !diff.captureError)) {
|
|
617
|
+
lines.push(`Full patches: ${patchesDir}`);
|
|
618
|
+
} else {
|
|
619
|
+
lines.push("Patch artifacts unavailable; preserved worktrees contain the recoverable changes.");
|
|
620
|
+
}
|
|
621
|
+
return lines.join("\n").trimEnd();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function formatParallelWorktreeSummary(
|
|
625
|
+
worktreeSetup: WorktreeSetup | undefined,
|
|
626
|
+
diffsDir: string,
|
|
627
|
+
agents: string[],
|
|
628
|
+
): string {
|
|
629
|
+
if (!worktreeSetup) return "";
|
|
630
|
+
return formatWorktreeDiffSummary(diffWorktrees(worktreeSetup, agents, diffsDir));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function appendWorktreeSummary(output: string, worktreeSummary: string): string {
|
|
634
|
+
return worktreeSummary ? `${output}\n\n${worktreeSummary}` : output;
|
|
635
|
+
}
|