pi-crew 0.2.3 → 0.2.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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
package/src/worktree/cleanup.ts
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
-
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
|
-
import { projectCrewRoot } from "../utils/paths.ts";
|
|
7
|
-
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
8
|
-
|
|
9
|
-
export interface WorktreeCleanupResult {
|
|
10
|
-
removed: string[];
|
|
11
|
-
preserved: Array<{ path: string; reason: string }>;
|
|
12
|
-
artifactPaths: string[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function git(cwd: string, args: string[]): string {
|
|
16
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } }).trim();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isDirty(worktreePath: string): boolean {
|
|
20
|
-
try {
|
|
21
|
-
return git(worktreePath, ["status", "--porcelain"]).trim().length > 0;
|
|
22
|
-
} catch {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function captureDiff(worktreePath: string): string {
|
|
28
|
-
try {
|
|
29
|
-
return [git(worktreePath, ["status", "--porcelain"]), "", git(worktreePath, ["diff", "--stat"]), "", git(worktreePath, ["diff"])].join("\n");
|
|
30
|
-
} catch (error) {
|
|
31
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
-
return `Failed to capture cleanup diff for ${worktreePath}: ${message}`;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function cleanupRunWorktrees(manifest: TeamRunManifest, options: { force?: boolean; signal?: AbortSignal } = {}): WorktreeCleanupResult {
|
|
37
|
-
const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, manifest.runId);
|
|
38
|
-
const result: WorktreeCleanupResult = { removed: [], preserved: [], artifactPaths: [] };
|
|
39
|
-
if (!fs.existsSync(worktreeRoot)) return result;
|
|
40
|
-
|
|
41
|
-
for (const entry of fs.readdirSync(worktreeRoot)) {
|
|
42
|
-
if (options.signal?.aborted) break;
|
|
43
|
-
const worktreePath = path.join(worktreeRoot, entry);
|
|
44
|
-
if (!fs.statSync(worktreePath).isDirectory()) continue;
|
|
45
|
-
const dirty = isDirty(worktreePath);
|
|
46
|
-
if (dirty && !options.force) {
|
|
47
|
-
const artifact = writeArtifact(manifest.artifactsRoot, {
|
|
48
|
-
kind: "diff",
|
|
49
|
-
relativePath: `cleanup/${entry}.diff`,
|
|
50
|
-
content: captureDiff(worktreePath),
|
|
51
|
-
producer: "worktree-cleanup",
|
|
52
|
-
});
|
|
53
|
-
result.artifactPaths.push(artifact.path);
|
|
54
|
-
result.preserved.push({ path: worktreePath, reason: "dirty worktree preserved" });
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const args = ["worktree", "remove"];
|
|
58
|
-
if (options.force) args.push("--force");
|
|
59
|
-
args.push(worktreePath);
|
|
60
|
-
try {
|
|
61
|
-
git(manifest.cwd, args);
|
|
62
|
-
result.removed.push(worktreePath);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
-
result.preserved.push({ path: worktreePath, reason: message });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
if (fs.existsSync(worktreeRoot) && fs.readdirSync(worktreeRoot).length === 0) fs.rmSync(worktreeRoot, { recursive: true, force: true });
|
|
71
|
-
} catch {
|
|
72
|
-
// Non-critical cleanup.
|
|
73
|
-
}
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
+
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
7
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
8
|
+
|
|
9
|
+
export interface WorktreeCleanupResult {
|
|
10
|
+
removed: string[];
|
|
11
|
+
preserved: Array<{ path: string; reason: string }>;
|
|
12
|
+
artifactPaths: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function git(cwd: string, args: string[]): string {
|
|
16
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } }).trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isDirty(worktreePath: string): boolean {
|
|
20
|
+
try {
|
|
21
|
+
return git(worktreePath, ["status", "--porcelain"]).trim().length > 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function captureDiff(worktreePath: string): string {
|
|
28
|
+
try {
|
|
29
|
+
return [git(worktreePath, ["status", "--porcelain"]), "", git(worktreePath, ["diff", "--stat"]), "", git(worktreePath, ["diff"])].join("\n");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return `Failed to capture cleanup diff for ${worktreePath}: ${message}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function cleanupRunWorktrees(manifest: TeamRunManifest, options: { force?: boolean; signal?: AbortSignal } = {}): WorktreeCleanupResult {
|
|
37
|
+
const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, manifest.runId);
|
|
38
|
+
const result: WorktreeCleanupResult = { removed: [], preserved: [], artifactPaths: [] };
|
|
39
|
+
if (!fs.existsSync(worktreeRoot)) return result;
|
|
40
|
+
|
|
41
|
+
for (const entry of fs.readdirSync(worktreeRoot)) {
|
|
42
|
+
if (options.signal?.aborted) break;
|
|
43
|
+
const worktreePath = path.join(worktreeRoot, entry);
|
|
44
|
+
if (!fs.statSync(worktreePath).isDirectory()) continue;
|
|
45
|
+
const dirty = isDirty(worktreePath);
|
|
46
|
+
if (dirty && !options.force) {
|
|
47
|
+
const artifact = writeArtifact(manifest.artifactsRoot, {
|
|
48
|
+
kind: "diff",
|
|
49
|
+
relativePath: `cleanup/${entry}.diff`,
|
|
50
|
+
content: captureDiff(worktreePath),
|
|
51
|
+
producer: "worktree-cleanup",
|
|
52
|
+
});
|
|
53
|
+
result.artifactPaths.push(artifact.path);
|
|
54
|
+
result.preserved.push({ path: worktreePath, reason: "dirty worktree preserved" });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const args = ["worktree", "remove"];
|
|
58
|
+
if (options.force) args.push("--force");
|
|
59
|
+
args.push(worktreePath);
|
|
60
|
+
try {
|
|
61
|
+
git(manifest.cwd, args);
|
|
62
|
+
result.removed.push(worktreePath);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
result.preserved.push({ path: worktreePath, reason: message });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (fs.existsSync(worktreeRoot) && fs.readdirSync(worktreeRoot).length === 0) fs.rmSync(worktreeRoot, { recursive: true, force: true });
|
|
71
|
+
} catch {
|
|
72
|
+
// Non-critical cleanup.
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { loadConfig } from "../config/config.ts";
|
|
5
|
-
import { projectCrewRoot } from "../utils/paths.ts";
|
|
6
|
-
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
7
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
-
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
9
|
-
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
10
|
-
|
|
11
|
-
export interface PreparedTaskWorkspace {
|
|
12
|
-
cwd: string;
|
|
13
|
-
worktreePath?: string;
|
|
14
|
-
branch?: string;
|
|
15
|
-
reused?: boolean;
|
|
16
|
-
nodeModulesLinked?: boolean;
|
|
17
|
-
syntheticPaths?: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface WorktreeDiffStat {
|
|
21
|
-
filesChanged: number;
|
|
22
|
-
insertions: number;
|
|
23
|
-
deletions: number;
|
|
24
|
-
diffStat: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function git(cwd: string, args: string[]): string {
|
|
28
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } }).trim();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function sanitizeBranchPart(value: string): string {
|
|
32
|
-
return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function findGitRoot(cwd: string): string {
|
|
36
|
-
return git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function assertCleanLeader(repoRoot: string): void {
|
|
40
|
-
const status = git(repoRoot, ["status", "--porcelain"]);
|
|
41
|
-
if (status.trim()) {
|
|
42
|
-
throw new Error("Worktree mode requires a clean leader repository. Commit/stash changes or use workspaceMode: 'single'.");
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boolean {
|
|
47
|
-
const source = path.join(repoRoot, "node_modules");
|
|
48
|
-
const target = path.join(worktreePath, "node_modules");
|
|
49
|
-
let sourceStat: fs.Stats;
|
|
50
|
-
try { sourceStat = fs.statSync(source); } catch { return false; }
|
|
51
|
-
if (!sourceStat.isDirectory()) return false;
|
|
52
|
-
if (fs.existsSync(target)) return false;
|
|
53
|
-
try {
|
|
54
|
-
fs.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
55
|
-
return true;
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
|
|
62
|
-
const resolved = path.resolve(worktreePath, rawPath);
|
|
63
|
-
const relative = path.relative(worktreePath, resolved);
|
|
64
|
-
if (!relative || relative === "." || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`synthetic path escapes worktree: ${rawPath}`);
|
|
65
|
-
return path.normalize(relative);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot: string, worktreePath: string, branch: string): string[] {
|
|
69
|
-
const cfg = loadConfig(manifest.cwd).config.worktree;
|
|
70
|
-
if (!cfg?.setupHook) return [];
|
|
71
|
-
const hookPath = path.isAbsolute(cfg.setupHook) ? cfg.setupHook : path.resolve(repoRoot, cfg.setupHook);
|
|
72
|
-
if (!fs.existsSync(hookPath) || fs.statSync(hookPath).isDirectory()) throw new Error(`worktree setup hook not found or not a file: ${hookPath}`);
|
|
73
|
-
const nodeHook = hookPath.endsWith(".js") || hookPath.endsWith(".cjs") || hookPath.endsWith(".mjs");
|
|
74
|
-
const result = spawnSync(nodeHook ? process.execPath : hookPath, nodeHook ? [hookPath] : [], {
|
|
75
|
-
cwd: worktreePath,
|
|
76
|
-
encoding: "utf-8",
|
|
77
|
-
input: JSON.stringify({ version: 1, repoRoot, worktreePath, agentCwd: worktreePath, branch, runId: manifest.runId, taskId: task.id, agent: task.agent }),
|
|
78
|
-
timeout: cfg.setupHookTimeoutMs ?? 30_000,
|
|
79
|
-
shell: false,
|
|
80
|
-
env: sanitizeEnvSecrets(process.env, {
|
|
81
|
-
allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL", "PI_*"],
|
|
82
|
-
}),
|
|
83
|
-
});
|
|
84
|
-
if (result.error) throw new Error(`worktree setup hook failed: ${result.error.message}`);
|
|
85
|
-
if (result.status !== 0) throw new Error(`worktree setup hook failed with exit code ${result.status}: ${result.stderr || result.stdout || "no output"}`);
|
|
86
|
-
const trimmed = result.stdout.trim();
|
|
87
|
-
if (!trimmed) return [];
|
|
88
|
-
try {
|
|
89
|
-
// Extract JSON from last line — hooks may output debug logging before JSON
|
|
90
|
-
const lines = trimmed.split(/\r?\n/);
|
|
91
|
-
const lastLine = lines[lines.length - 1] ?? trimmed;
|
|
92
|
-
const parsed = JSON.parse(lastLine) as { syntheticPaths?: unknown };
|
|
93
|
-
if (!Array.isArray(parsed.syntheticPaths)) return [];
|
|
94
|
-
return [...new Set(parsed.syntheticPaths.filter((entry): entry is string => typeof entry === "string").map((entry) => normalizeSyntheticPath(worktreePath, entry)))];
|
|
95
|
-
} catch (error) {
|
|
96
|
-
logInternalError("worktree.setupHook.parse", error, `lastLine=${(trimmed.split(/\r?\n/).pop() ?? "").slice(0, 200)}`);
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function branchExists(repoRoot: string, branch: string): { local: boolean; remoteOnly: boolean } {
|
|
102
|
-
let local = false;
|
|
103
|
-
try { git(repoRoot, ["rev-parse", "--verify", `refs/heads/${branch}`]); local = true; } catch {}
|
|
104
|
-
if (local) return { local: true, remoteOnly: false };
|
|
105
|
-
// Check remote-tracking branch
|
|
106
|
-
try {
|
|
107
|
-
const out = execFileSync("git", ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`],
|
|
108
|
-
{ cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
109
|
-
return { local: false, remoteOnly: out.length > 0 };
|
|
110
|
-
} catch { return { local: false, remoteOnly: false }; }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function pruneStaleWorktrees(repoRoot: string): void {
|
|
114
|
-
try { execFileSync("git", ["worktree", "prune"], { cwd: repoRoot, stdio: "ignore" }); }
|
|
115
|
-
catch { /* best-effort */ }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState): PreparedTaskWorkspace {
|
|
119
|
-
if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
|
|
120
|
-
const repoRoot = findGitRoot(manifest.cwd);
|
|
121
|
-
const loadedConfig = loadConfig(manifest.cwd);
|
|
122
|
-
if (loadedConfig.config.requireCleanWorktreeLeader !== false) assertCleanLeader(repoRoot);
|
|
123
|
-
const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, manifest.runId);
|
|
124
|
-
fs.mkdirSync(worktreeRoot, { recursive: true });
|
|
125
|
-
const worktreePath = path.join(worktreeRoot, task.id);
|
|
126
|
-
const branch = `pi-crew/${sanitizeBranchPart(manifest.runId)}/${sanitizeBranchPart(task.id)}`;
|
|
127
|
-
if (fs.existsSync(worktreePath)) {
|
|
128
|
-
let currentBranch: string;
|
|
129
|
-
try {
|
|
130
|
-
currentBranch = git(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
131
|
-
} catch (gitError) {
|
|
132
|
-
throw new Error(`Existing worktree at ${worktreePath} is not a valid git repository; cannot verify branch: ${gitError instanceof Error ? gitError.message : String(gitError)}`);
|
|
133
|
-
}
|
|
134
|
-
if (currentBranch !== branch) {
|
|
135
|
-
throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
|
|
136
|
-
}
|
|
137
|
-
return { cwd: worktreePath, worktreePath, branch, reused: true };
|
|
138
|
-
}
|
|
139
|
-
pruneStaleWorktrees(repoRoot);
|
|
140
|
-
const exists = branchExists(repoRoot, branch);
|
|
141
|
-
try {
|
|
142
|
-
if (exists.local) {
|
|
143
|
-
git(repoRoot, ["worktree", "add", worktreePath, branch]);
|
|
144
|
-
} else {
|
|
145
|
-
if (exists.remoteOnly) {
|
|
146
|
-
logInternalError("worktree.branchRemoteOnly", new Error(`Branch '${branch}' exists only on remote; creating local from HEAD instead of tracking remote.`), `branch=${branch}`);
|
|
147
|
-
}
|
|
148
|
-
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
152
|
-
if (/already checked out|is already used by worktree/i.test(msg)) {
|
|
153
|
-
throw new Error(`Branch '${branch}' is checked out at another worktree. Run \`team cleanup runId=${manifest.runId} force=true\` or manually remove the conflicting worktree.`);
|
|
154
|
-
}
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
|
-
const syntheticPaths = runSetupHook(manifest, task, repoRoot, worktreePath, branch);
|
|
158
|
-
const nodeModulesLinked = loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
|
|
159
|
-
return { cwd: worktreePath, worktreePath, branch, reused: false, nodeModulesLinked, syntheticPaths };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function captureWorktreeDiffStat(worktreePath: string): WorktreeDiffStat {
|
|
163
|
-
try {
|
|
164
|
-
const diffStat = git(worktreePath, ["diff", "--stat"]);
|
|
165
|
-
const numstat = git(worktreePath, ["diff", "--numstat"]);
|
|
166
|
-
let filesChanged = 0;
|
|
167
|
-
let insertions = 0;
|
|
168
|
-
let deletions = 0;
|
|
169
|
-
for (const line of numstat.split(/\r?\n/).filter(Boolean)) {
|
|
170
|
-
const [add, del] = line.split(/\s+/);
|
|
171
|
-
filesChanged += 1;
|
|
172
|
-
insertions += Number(add) || 0;
|
|
173
|
-
deletions += Number(del) || 0;
|
|
174
|
-
}
|
|
175
|
-
return { filesChanged, insertions, deletions, diffStat };
|
|
176
|
-
} catch {
|
|
177
|
-
return { filesChanged: 0, insertions: 0, deletions: 0, diffStat: "" };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function captureWorktreeDiff(worktreePath: string): string {
|
|
182
|
-
try {
|
|
183
|
-
return git(worktreePath, ["diff", "--stat"]) + "\n\n" + git(worktreePath, ["diff"]);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
-
return `Failed to capture worktree diff: ${message}`;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { loadConfig } from "../config/config.ts";
|
|
5
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
6
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
7
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
+
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
9
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
10
|
+
|
|
11
|
+
export interface PreparedTaskWorkspace {
|
|
12
|
+
cwd: string;
|
|
13
|
+
worktreePath?: string;
|
|
14
|
+
branch?: string;
|
|
15
|
+
reused?: boolean;
|
|
16
|
+
nodeModulesLinked?: boolean;
|
|
17
|
+
syntheticPaths?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorktreeDiffStat {
|
|
21
|
+
filesChanged: number;
|
|
22
|
+
insertions: number;
|
|
23
|
+
deletions: number;
|
|
24
|
+
diffStat: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function git(cwd: string, args: string[]): string {
|
|
28
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } }).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeBranchPart(value: string): string {
|
|
32
|
+
return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findGitRoot(cwd: string): string {
|
|
36
|
+
return git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertCleanLeader(repoRoot: string): void {
|
|
40
|
+
const status = git(repoRoot, ["status", "--porcelain"]);
|
|
41
|
+
if (status.trim()) {
|
|
42
|
+
throw new Error("Worktree mode requires a clean leader repository. Commit/stash changes or use workspaceMode: 'single'.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boolean {
|
|
47
|
+
const source = path.join(repoRoot, "node_modules");
|
|
48
|
+
const target = path.join(worktreePath, "node_modules");
|
|
49
|
+
let sourceStat: fs.Stats;
|
|
50
|
+
try { sourceStat = fs.statSync(source); } catch { return false; }
|
|
51
|
+
if (!sourceStat.isDirectory()) return false;
|
|
52
|
+
if (fs.existsSync(target)) return false;
|
|
53
|
+
try {
|
|
54
|
+
fs.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
|
|
62
|
+
const resolved = path.resolve(worktreePath, rawPath);
|
|
63
|
+
const relative = path.relative(worktreePath, resolved);
|
|
64
|
+
if (!relative || relative === "." || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`synthetic path escapes worktree: ${rawPath}`);
|
|
65
|
+
return path.normalize(relative);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot: string, worktreePath: string, branch: string): string[] {
|
|
69
|
+
const cfg = loadConfig(manifest.cwd).config.worktree;
|
|
70
|
+
if (!cfg?.setupHook) return [];
|
|
71
|
+
const hookPath = path.isAbsolute(cfg.setupHook) ? cfg.setupHook : path.resolve(repoRoot, cfg.setupHook);
|
|
72
|
+
if (!fs.existsSync(hookPath) || fs.statSync(hookPath).isDirectory()) throw new Error(`worktree setup hook not found or not a file: ${hookPath}`);
|
|
73
|
+
const nodeHook = hookPath.endsWith(".js") || hookPath.endsWith(".cjs") || hookPath.endsWith(".mjs");
|
|
74
|
+
const result = spawnSync(nodeHook ? process.execPath : hookPath, nodeHook ? [hookPath] : [], {
|
|
75
|
+
cwd: worktreePath,
|
|
76
|
+
encoding: "utf-8",
|
|
77
|
+
input: JSON.stringify({ version: 1, repoRoot, worktreePath, agentCwd: worktreePath, branch, runId: manifest.runId, taskId: task.id, agent: task.agent }),
|
|
78
|
+
timeout: cfg.setupHookTimeoutMs ?? 30_000,
|
|
79
|
+
shell: false,
|
|
80
|
+
env: sanitizeEnvSecrets(process.env, {
|
|
81
|
+
allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL", "PI_*"],
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
if (result.error) throw new Error(`worktree setup hook failed: ${result.error.message}`);
|
|
85
|
+
if (result.status !== 0) throw new Error(`worktree setup hook failed with exit code ${result.status}: ${result.stderr || result.stdout || "no output"}`);
|
|
86
|
+
const trimmed = result.stdout.trim();
|
|
87
|
+
if (!trimmed) return [];
|
|
88
|
+
try {
|
|
89
|
+
// Extract JSON from last line — hooks may output debug logging before JSON
|
|
90
|
+
const lines = trimmed.split(/\r?\n/);
|
|
91
|
+
const lastLine = lines[lines.length - 1] ?? trimmed;
|
|
92
|
+
const parsed = JSON.parse(lastLine) as { syntheticPaths?: unknown };
|
|
93
|
+
if (!Array.isArray(parsed.syntheticPaths)) return [];
|
|
94
|
+
return [...new Set(parsed.syntheticPaths.filter((entry): entry is string => typeof entry === "string").map((entry) => normalizeSyntheticPath(worktreePath, entry)))];
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logInternalError("worktree.setupHook.parse", error, `lastLine=${(trimmed.split(/\r?\n/).pop() ?? "").slice(0, 200)}`);
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function branchExists(repoRoot: string, branch: string): { local: boolean; remoteOnly: boolean } {
|
|
102
|
+
let local = false;
|
|
103
|
+
try { git(repoRoot, ["rev-parse", "--verify", `refs/heads/${branch}`]); local = true; } catch {}
|
|
104
|
+
if (local) return { local: true, remoteOnly: false };
|
|
105
|
+
// Check remote-tracking branch
|
|
106
|
+
try {
|
|
107
|
+
const out = execFileSync("git", ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`],
|
|
108
|
+
{ cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
109
|
+
return { local: false, remoteOnly: out.length > 0 };
|
|
110
|
+
} catch { return { local: false, remoteOnly: false }; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function pruneStaleWorktrees(repoRoot: string): void {
|
|
114
|
+
try { execFileSync("git", ["worktree", "prune"], { cwd: repoRoot, stdio: "ignore" }); }
|
|
115
|
+
catch { /* best-effort */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState): PreparedTaskWorkspace {
|
|
119
|
+
if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
|
|
120
|
+
const repoRoot = findGitRoot(manifest.cwd);
|
|
121
|
+
const loadedConfig = loadConfig(manifest.cwd);
|
|
122
|
+
if (loadedConfig.config.requireCleanWorktreeLeader !== false) assertCleanLeader(repoRoot);
|
|
123
|
+
const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, manifest.runId);
|
|
124
|
+
fs.mkdirSync(worktreeRoot, { recursive: true });
|
|
125
|
+
const worktreePath = path.join(worktreeRoot, task.id);
|
|
126
|
+
const branch = `pi-crew/${sanitizeBranchPart(manifest.runId)}/${sanitizeBranchPart(task.id)}`;
|
|
127
|
+
if (fs.existsSync(worktreePath)) {
|
|
128
|
+
let currentBranch: string;
|
|
129
|
+
try {
|
|
130
|
+
currentBranch = git(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
131
|
+
} catch (gitError) {
|
|
132
|
+
throw new Error(`Existing worktree at ${worktreePath} is not a valid git repository; cannot verify branch: ${gitError instanceof Error ? gitError.message : String(gitError)}`);
|
|
133
|
+
}
|
|
134
|
+
if (currentBranch !== branch) {
|
|
135
|
+
throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
|
|
136
|
+
}
|
|
137
|
+
return { cwd: worktreePath, worktreePath, branch, reused: true };
|
|
138
|
+
}
|
|
139
|
+
pruneStaleWorktrees(repoRoot);
|
|
140
|
+
const exists = branchExists(repoRoot, branch);
|
|
141
|
+
try {
|
|
142
|
+
if (exists.local) {
|
|
143
|
+
git(repoRoot, ["worktree", "add", worktreePath, branch]);
|
|
144
|
+
} else {
|
|
145
|
+
if (exists.remoteOnly) {
|
|
146
|
+
logInternalError("worktree.branchRemoteOnly", new Error(`Branch '${branch}' exists only on remote; creating local from HEAD instead of tracking remote.`), `branch=${branch}`);
|
|
147
|
+
}
|
|
148
|
+
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
152
|
+
if (/already checked out|is already used by worktree/i.test(msg)) {
|
|
153
|
+
throw new Error(`Branch '${branch}' is checked out at another worktree. Run \`team cleanup runId=${manifest.runId} force=true\` or manually remove the conflicting worktree.`);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
const syntheticPaths = runSetupHook(manifest, task, repoRoot, worktreePath, branch);
|
|
158
|
+
const nodeModulesLinked = loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
|
|
159
|
+
return { cwd: worktreePath, worktreePath, branch, reused: false, nodeModulesLinked, syntheticPaths };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function captureWorktreeDiffStat(worktreePath: string): WorktreeDiffStat {
|
|
163
|
+
try {
|
|
164
|
+
const diffStat = git(worktreePath, ["diff", "--stat"]);
|
|
165
|
+
const numstat = git(worktreePath, ["diff", "--numstat"]);
|
|
166
|
+
let filesChanged = 0;
|
|
167
|
+
let insertions = 0;
|
|
168
|
+
let deletions = 0;
|
|
169
|
+
for (const line of numstat.split(/\r?\n/).filter(Boolean)) {
|
|
170
|
+
const [add, del] = line.split(/\s+/);
|
|
171
|
+
filesChanged += 1;
|
|
172
|
+
insertions += Number(add) || 0;
|
|
173
|
+
deletions += Number(del) || 0;
|
|
174
|
+
}
|
|
175
|
+
return { filesChanged, insertions, deletions, diffStat };
|
|
176
|
+
} catch {
|
|
177
|
+
return { filesChanged: 0, insertions: 0, deletions: 0, diffStat: "" };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function captureWorktreeDiff(worktreePath: string): string {
|
|
182
|
+
try {
|
|
183
|
+
return git(worktreePath, ["diff", "--stat"]) + "\n\n" + git(worktreePath, ["diff"]);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
return `Failed to capture worktree diff: ${message}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
package/teams/default.team.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: default
|
|
3
|
-
description: Balanced team for ordinary implementation tasks
|
|
4
|
-
defaultWorkflow: default
|
|
5
|
-
workspaceMode: single
|
|
6
|
-
maxConcurrency: 2
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
- explorer: agent=explorer fast discovery
|
|
10
|
-
- planner: agent=planner plan the work
|
|
11
|
-
- executor: agent=executor implement changes
|
|
12
|
-
- verifier: agent=verifier verify completion
|
|
1
|
+
---
|
|
2
|
+
name: default
|
|
3
|
+
description: Balanced team for ordinary implementation tasks
|
|
4
|
+
defaultWorkflow: default
|
|
5
|
+
workspaceMode: single
|
|
6
|
+
maxConcurrency: 2
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
- explorer: agent=explorer fast discovery
|
|
10
|
+
- planner: agent=planner plan the work
|
|
11
|
+
- executor: agent=executor implement changes
|
|
12
|
+
- verifier: agent=verifier verify completion
|
package/teams/fast-fix.team.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: fast-fix
|
|
3
|
-
description: Small team for quick bug fixes
|
|
4
|
-
defaultWorkflow: fast-fix
|
|
5
|
-
workspaceMode: single
|
|
6
|
-
maxConcurrency: 1
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
- explorer: agent=explorer find the relevant files
|
|
10
|
-
- executor: agent=executor make the fix
|
|
11
|
-
- verifier: agent=verifier verify the fix
|
|
1
|
+
---
|
|
2
|
+
name: fast-fix
|
|
3
|
+
description: Small team for quick bug fixes
|
|
4
|
+
defaultWorkflow: fast-fix
|
|
5
|
+
workspaceMode: single
|
|
6
|
+
maxConcurrency: 1
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
- explorer: agent=explorer find the relevant files
|
|
10
|
+
- executor: agent=executor make the fix
|
|
11
|
+
- verifier: agent=verifier verify the fix
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: implementation
|
|
3
|
-
description: Full implementation team with parallel specialists, critique, execution, review, and verification
|
|
4
|
-
defaultWorkflow: implementation
|
|
5
|
-
workspaceMode: single
|
|
6
|
-
maxConcurrency: 3
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
- explorer: agent=explorer map the codebase
|
|
10
|
-
- analyst: agent=analyst clarify requirements and constraints
|
|
11
|
-
- planner: agent=planner create execution plan
|
|
12
|
-
- critic: agent=critic challenge and synthesize specialist findings
|
|
13
|
-
- executor: agent=executor implement the plan
|
|
14
|
-
- reviewer: agent=reviewer review the implementation
|
|
15
|
-
- security-reviewer: agent=security-reviewer review security and trust boundaries
|
|
16
|
-
- test-engineer: agent=test-engineer design and run verification
|
|
17
|
-
- verifier: agent=verifier verify done
|
|
18
|
-
- writer: agent=writer summarize documentation or release notes when needed
|
|
1
|
+
---
|
|
2
|
+
name: implementation
|
|
3
|
+
description: Full implementation team with parallel specialists, critique, execution, review, and verification
|
|
4
|
+
defaultWorkflow: implementation
|
|
5
|
+
workspaceMode: single
|
|
6
|
+
maxConcurrency: 3
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
- explorer: agent=explorer map the codebase
|
|
10
|
+
- analyst: agent=analyst clarify requirements and constraints
|
|
11
|
+
- planner: agent=planner create execution plan
|
|
12
|
+
- critic: agent=critic challenge and synthesize specialist findings
|
|
13
|
+
- executor: agent=executor implement the plan
|
|
14
|
+
- reviewer: agent=reviewer review the implementation
|
|
15
|
+
- security-reviewer: agent=security-reviewer review security and trust boundaries
|
|
16
|
+
- test-engineer: agent=test-engineer design and run verification
|
|
17
|
+
- verifier: agent=verifier verify done
|
|
18
|
+
- writer: agent=writer summarize documentation or release notes when needed
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: parallel-research
|
|
3
|
-
description: Parallel research team for multi-project/source audits
|
|
4
|
-
workspaceMode: single
|
|
5
|
-
defaultWorkflow: parallel-research
|
|
6
|
-
maxConcurrency: 4
|
|
7
|
-
triggers: đọc sâu, deep read, deep research, source audit, multiple projects, parallel research, pi-*
|
|
8
|
-
category: research
|
|
9
|
-
cost: cheap
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
- explorer: agent=explorer gather source facts in parallel shards
|
|
13
|
-
- analyst: agent=analyst synthesize shard findings
|
|
14
|
-
- writer: agent=writer produce final notes
|
|
1
|
+
---
|
|
2
|
+
name: parallel-research
|
|
3
|
+
description: Parallel research team for multi-project/source audits
|
|
4
|
+
workspaceMode: single
|
|
5
|
+
defaultWorkflow: parallel-research
|
|
6
|
+
maxConcurrency: 4
|
|
7
|
+
triggers: đọc sâu, deep read, deep research, source audit, multiple projects, parallel research, pi-*
|
|
8
|
+
category: research
|
|
9
|
+
cost: cheap
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
- explorer: agent=explorer gather source facts in parallel shards
|
|
13
|
+
- analyst: agent=analyst synthesize shard findings
|
|
14
|
+
- writer: agent=writer produce final notes
|