pi-crew 0.1.7 → 0.1.8
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/package.json +1 -1
- package/src/agents/agent-config.ts +1 -0
- package/src/agents/discover-agents.ts +5 -0
- package/src/config/config.ts +25 -0
- package/src/extension/async-notifier.ts +3 -1
- package/src/extension/cross-extension-rpc.ts +82 -0
- package/src/extension/register.ts +9 -1
- package/src/extension/result-watcher.ts +89 -0
- package/src/extension/team-tool.ts +105 -11
- package/src/runtime/agent-memory.ts +72 -0
- package/src/runtime/live-agent-control.ts +78 -0
- package/src/runtime/live-agent-manager.ts +85 -0
- package/src/runtime/live-control-realtime.ts +36 -0
- package/src/runtime/live-session-runtime.ts +271 -5
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/sidechain-output.ts +28 -0
- package/src/runtime/task-runner.ts +77 -12
- package/src/runtime/team-runner.ts +4 -1
- package/src/utils/file-coalescer.ts +33 -0
- package/src/worktree/worktree-manager.ts +73 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
3
|
-
import type { CrewLimitsConfig } from "../config/config.ts";
|
|
3
|
+
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
4
4
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
5
5
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
@@ -8,7 +8,7 @@ import { saveRunManifest, saveRunTasks } from "../state/state-store.ts";
|
|
|
8
8
|
import { createTaskClaim } from "../state/task-claims.ts";
|
|
9
9
|
import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
|
|
10
10
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
11
|
-
import { captureWorktreeDiff, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
11
|
+
import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
12
12
|
import { buildModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
|
|
13
13
|
import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
|
|
14
14
|
import { runChildPi } from "./child-pi.ts";
|
|
@@ -19,7 +19,9 @@ import { permissionForRole } from "./role-permission.ts";
|
|
|
19
19
|
import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
|
|
20
20
|
import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
|
|
21
21
|
import { parseSessionUsage } from "./session-usage.ts";
|
|
22
|
-
import type { CrewAgentProgress } from "./crew-agent-runtime.ts";
|
|
22
|
+
import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
23
|
+
import { buildMemoryBlock } from "./agent-memory.ts";
|
|
24
|
+
import { runLiveSessionTask } from "./live-session-runtime.ts";
|
|
23
25
|
|
|
24
26
|
export interface TaskRunnerInput {
|
|
25
27
|
manifest: TeamRunManifest;
|
|
@@ -29,6 +31,11 @@ export interface TaskRunnerInput {
|
|
|
29
31
|
agent: AgentConfig;
|
|
30
32
|
signal?: AbortSignal;
|
|
31
33
|
executeWorkers: boolean;
|
|
34
|
+
runtimeKind?: CrewRuntimeKind;
|
|
35
|
+
runtimeConfig?: CrewRuntimeConfig;
|
|
36
|
+
parentContext?: string;
|
|
37
|
+
parentModel?: unknown;
|
|
38
|
+
modelRegistry?: unknown;
|
|
32
39
|
limits?: CrewLimitsConfig;
|
|
33
40
|
dependencyContextText?: string;
|
|
34
41
|
}
|
|
@@ -57,7 +64,8 @@ function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
|
57
64
|
].join("\n");
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): string {
|
|
67
|
+
function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState, agent?: AgentConfig): string {
|
|
68
|
+
const memoryBlock = agent?.memory ? buildMemoryBlock(agent.name, agent.memory, task.cwd, Boolean(agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
|
|
61
69
|
return [
|
|
62
70
|
"# pi-crew Worker Runtime Context",
|
|
63
71
|
`Run ID: ${manifest.runId}`,
|
|
@@ -88,6 +96,7 @@ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: T
|
|
|
88
96
|
task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
|
|
89
97
|
"",
|
|
90
98
|
(inputDependencyContext(task) || ""),
|
|
99
|
+
memoryBlock,
|
|
91
100
|
"Task:",
|
|
92
101
|
step.task.replaceAll("{goal}", manifest.goal),
|
|
93
102
|
].join("\n");
|
|
@@ -227,12 +236,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
227
236
|
...(dependencyContextText ? { dependencyContextText } : {}),
|
|
228
237
|
} as TeamTaskState;
|
|
229
238
|
let tasks = updateTask(input.tasks, task);
|
|
239
|
+
const runtimeKind = input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
230
240
|
saveRunTasks(manifest, tasks);
|
|
231
|
-
upsertCrewAgent(manifest, recordFromTask(manifest, task,
|
|
232
|
-
appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
|
|
241
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
|
|
242
|
+
appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, runtime: runtimeKind, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
|
|
233
243
|
const permissionMode = permissionForRole(task.role);
|
|
234
244
|
|
|
235
|
-
const prompt = renderTaskPrompt(manifest, input.step, task);
|
|
245
|
+
const prompt = renderTaskPrompt(manifest, input.step, task, input.agent);
|
|
236
246
|
const promptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
237
247
|
kind: "prompt",
|
|
238
248
|
relativePath: `prompts/${task.id}.md`,
|
|
@@ -248,7 +258,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
248
258
|
let modelAttempts: ModelAttemptSummary[] | undefined;
|
|
249
259
|
let parsedOutput: ParsedPiJsonOutput | undefined;
|
|
250
260
|
|
|
251
|
-
let startupEvidence = createStartupEvidence({ command:
|
|
261
|
+
let startupEvidence = createStartupEvidence({ command: runtimeKind === "child-process" ? "pi" : runtimeKind === "live-session" ? "live-session" : "safe-scaffold", startedAt: new Date(task.startedAt ?? new Date().toISOString()), finishedAt: new Date(), promptSentAt: new Date(task.startedAt ?? new Date().toISOString()), promptAccepted: true, exitCode: 0 });
|
|
252
262
|
const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
|
|
253
263
|
const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
254
264
|
kind: "metadata",
|
|
@@ -256,7 +266,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
256
266
|
content: `${coordinationBridgeInstructions(task)}\n`,
|
|
257
267
|
producer: task.id,
|
|
258
268
|
});
|
|
259
|
-
if (
|
|
269
|
+
if (runtimeKind === "child-process") {
|
|
260
270
|
const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
|
|
261
271
|
const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
|
|
262
272
|
const logs: string[] = [];
|
|
@@ -326,6 +336,55 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
326
336
|
producer: task.id,
|
|
327
337
|
});
|
|
328
338
|
}
|
|
339
|
+
} else if (runtimeKind === "live-session") {
|
|
340
|
+
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
341
|
+
const attemptStartedAt = new Date();
|
|
342
|
+
const liveResult = await runLiveSessionTask({
|
|
343
|
+
manifest,
|
|
344
|
+
task,
|
|
345
|
+
step: input.step,
|
|
346
|
+
agent: input.agent,
|
|
347
|
+
prompt,
|
|
348
|
+
signal: input.signal,
|
|
349
|
+
transcriptPath,
|
|
350
|
+
runtimeConfig: input.runtimeConfig,
|
|
351
|
+
parentContext: input.parentContext,
|
|
352
|
+
parentModel: input.parentModel,
|
|
353
|
+
modelRegistry: input.modelRegistry,
|
|
354
|
+
onOutput: (text) => appendCrewAgentOutput(manifest, task.id, text),
|
|
355
|
+
onEvent: (event) => {
|
|
356
|
+
appendCrewAgentEvent(manifest, task.id, event);
|
|
357
|
+
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
358
|
+
tasks = updateTask(tasks, task);
|
|
359
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "live-session"));
|
|
360
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { event } });
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
startupEvidence = createStartupEvidence({ command: "live-session", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: liveResult.exitCode === 0 && !liveResult.error, stderr: liveResult.stderr, error: liveResult.error, exitCode: liveResult.exitCode });
|
|
364
|
+
exitCode = liveResult.exitCode;
|
|
365
|
+
error = liveResult.error || (liveResult.exitCode && liveResult.exitCode !== 0 ? liveResult.stderr || `Live session exited with ${liveResult.exitCode}` : undefined);
|
|
366
|
+
parsedOutput = { finalText: liveResult.stdout, textEvents: liveResult.stdout ? [liveResult.stdout] : [], jsonEvents: liveResult.jsonEvents, usage: liveResult.usage };
|
|
367
|
+
if (liveResult.usage) task = { ...task, usage: liveResult.usage, agentProgress: applyUsageToProgress(task.agentProgress, liveResult.usage) };
|
|
368
|
+
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
369
|
+
kind: "result",
|
|
370
|
+
relativePath: `results/${task.id}.txt`,
|
|
371
|
+
content: liveResult.stdout || liveResult.stderr || "(no output)",
|
|
372
|
+
producer: task.id,
|
|
373
|
+
});
|
|
374
|
+
logArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
375
|
+
kind: "log",
|
|
376
|
+
relativePath: `logs/${task.id}.log`,
|
|
377
|
+
content: [`runtime=live-session`, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${liveResult.jsonEvents}`, liveResult.usage ? `usage=${JSON.stringify(liveResult.usage)}` : "", "", "STDOUT:", liveResult.stdout, "", "STDERR:", liveResult.stderr].join("\n"),
|
|
378
|
+
producer: task.id,
|
|
379
|
+
});
|
|
380
|
+
if (fs.existsSync(transcriptPath)) {
|
|
381
|
+
transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
382
|
+
kind: "log",
|
|
383
|
+
relativePath: `transcripts/${task.id}.jsonl`,
|
|
384
|
+
content: fs.readFileSync(transcriptPath, "utf-8"),
|
|
385
|
+
producer: task.id,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
329
388
|
} else {
|
|
330
389
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
331
390
|
kind: "result",
|
|
@@ -346,6 +405,12 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
346
405
|
content: captureWorktreeDiff(workspace.worktreePath),
|
|
347
406
|
producer: task.id,
|
|
348
407
|
}) : undefined;
|
|
408
|
+
const diffStatArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
|
|
409
|
+
kind: "metadata",
|
|
410
|
+
relativePath: `metadata/${task.id}.diff-stat.json`,
|
|
411
|
+
content: `${JSON.stringify({ ...captureWorktreeDiffStat(workspace.worktreePath), syntheticPaths: workspace.syntheticPaths ?? [], nodeModulesLinked: workspace.nodeModulesLinked ?? false }, null, 2)}\n`,
|
|
412
|
+
producer: task.id,
|
|
413
|
+
}) : undefined;
|
|
349
414
|
|
|
350
415
|
task = {
|
|
351
416
|
...task,
|
|
@@ -357,7 +422,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
357
422
|
jsonEvents: parsedOutput?.jsonEvents,
|
|
358
423
|
agentProgress: error && task.agentProgress?.currentTool ? { ...task.agentProgress, failedTool: task.agentProgress.currentTool } : task.agentProgress,
|
|
359
424
|
error,
|
|
360
|
-
verification: createVerificationEvidence(taskPacket.verification, !error, error ? `Task failed: ${error}` :
|
|
425
|
+
verification: createVerificationEvidence(taskPacket.verification, !error, error ? `Task failed: ${error}` : runtimeKind === "scaffold" ? "Safe scaffold mode; verification commands were not executed." : `${runtimeKind} worker finished without reporting a verification failure.`),
|
|
361
426
|
promptArtifact,
|
|
362
427
|
resultArtifact,
|
|
363
428
|
claim: undefined,
|
|
@@ -391,10 +456,10 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
391
456
|
content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
|
|
392
457
|
producer: task.id,
|
|
393
458
|
});
|
|
394
|
-
manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
|
|
459
|
+
manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
|
|
395
460
|
saveRunManifest(manifest);
|
|
396
461
|
saveRunTasks(manifest, tasks);
|
|
397
|
-
upsertCrewAgent(manifest, recordFromTask(manifest, task,
|
|
462
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
|
|
398
463
|
appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
|
|
399
464
|
return { manifest, tasks };
|
|
400
465
|
}
|
|
@@ -27,6 +27,9 @@ export interface ExecuteTeamRunInput {
|
|
|
27
27
|
limits?: CrewLimitsConfig;
|
|
28
28
|
runtime?: CrewRuntimeCapabilities;
|
|
29
29
|
runtimeConfig?: CrewRuntimeConfig;
|
|
30
|
+
parentContext?: string;
|
|
31
|
+
parentModel?: unknown;
|
|
32
|
+
modelRegistry?: unknown;
|
|
30
33
|
signal?: AbortSignal;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -174,7 +177,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
174
177
|
const results = await Promise.all(readyBatch.map((task) => {
|
|
175
178
|
const step = findStep(input.workflow, task);
|
|
176
179
|
const agent = findAgent(input.agents, task);
|
|
177
|
-
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, limits: input.limits });
|
|
180
|
+
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, limits: input.limits });
|
|
178
181
|
}));
|
|
179
182
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
180
183
|
tasks = mergeTaskUpdates(tasks, results);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface TimerApi {
|
|
2
|
+
setTimeout(handler: () => void, delayMs: number): unknown;
|
|
3
|
+
clearTimeout(handle: unknown): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const defaultTimerApi: TimerApi = {
|
|
7
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
8
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface FileCoalescer {
|
|
12
|
+
schedule(file: string, delayMs?: number): boolean;
|
|
13
|
+
clear(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createFileCoalescer(handler: (file: string) => void, defaultDelayMs: number, timerApi: TimerApi = defaultTimerApi): FileCoalescer {
|
|
17
|
+
const pending = new Map<string, unknown>();
|
|
18
|
+
return {
|
|
19
|
+
schedule(file, delayMs = defaultDelayMs) {
|
|
20
|
+
if (pending.has(file)) return false;
|
|
21
|
+
const timer = timerApi.setTimeout(() => {
|
|
22
|
+
pending.delete(file);
|
|
23
|
+
handler(file);
|
|
24
|
+
}, delayMs);
|
|
25
|
+
pending.set(file, timer);
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
clear() {
|
|
29
|
+
for (const timer of pending.values()) timerApi.clearTimeout(timer);
|
|
30
|
+
pending.clear();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { loadConfig } from "../config/config.ts";
|
|
@@ -9,6 +9,15 @@ export interface PreparedTaskWorkspace {
|
|
|
9
9
|
worktreePath?: string;
|
|
10
10
|
branch?: string;
|
|
11
11
|
reused?: boolean;
|
|
12
|
+
nodeModulesLinked?: boolean;
|
|
13
|
+
syntheticPaths?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WorktreeDiffStat {
|
|
17
|
+
filesChanged: number;
|
|
18
|
+
insertions: number;
|
|
19
|
+
deletions: number;
|
|
20
|
+
diffStat: string;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
function git(cwd: string, args: string[]): string {
|
|
@@ -30,6 +39,47 @@ export function assertCleanLeader(repoRoot: string): void {
|
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boolean {
|
|
43
|
+
const source = path.join(repoRoot, "node_modules");
|
|
44
|
+
const target = path.join(worktreePath, "node_modules");
|
|
45
|
+
if (!fs.existsSync(source) || fs.existsSync(target)) return false;
|
|
46
|
+
try {
|
|
47
|
+
fs.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
|
|
55
|
+
const resolved = path.resolve(worktreePath, rawPath);
|
|
56
|
+
const relative = path.relative(worktreePath, resolved);
|
|
57
|
+
if (!relative || relative === "." || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`synthetic path escapes worktree: ${rawPath}`);
|
|
58
|
+
return path.normalize(relative);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot: string, worktreePath: string, branch: string): string[] {
|
|
62
|
+
const cfg = loadConfig(manifest.cwd).config.worktree;
|
|
63
|
+
if (!cfg?.setupHook) return [];
|
|
64
|
+
const hookPath = path.isAbsolute(cfg.setupHook) ? cfg.setupHook : path.resolve(repoRoot, cfg.setupHook);
|
|
65
|
+
if (!fs.existsSync(hookPath) || fs.statSync(hookPath).isDirectory()) throw new Error(`worktree setup hook not found or not a file: ${hookPath}`);
|
|
66
|
+
const nodeHook = hookPath.endsWith(".js") || hookPath.endsWith(".cjs") || hookPath.endsWith(".mjs");
|
|
67
|
+
const result = spawnSync(nodeHook ? process.execPath : hookPath, nodeHook ? [hookPath] : [], {
|
|
68
|
+
cwd: worktreePath,
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
input: JSON.stringify({ version: 1, repoRoot, worktreePath, agentCwd: worktreePath, branch, runId: manifest.runId, taskId: task.id, agent: task.agent }),
|
|
71
|
+
timeout: cfg.setupHookTimeoutMs ?? 30_000,
|
|
72
|
+
shell: false,
|
|
73
|
+
});
|
|
74
|
+
if (result.error) throw new Error(`worktree setup hook failed: ${result.error.message}`);
|
|
75
|
+
if (result.status !== 0) throw new Error(`worktree setup hook failed with exit code ${result.status}: ${result.stderr || result.stdout || "no output"}`);
|
|
76
|
+
const trimmed = result.stdout.trim();
|
|
77
|
+
if (!trimmed) return [];
|
|
78
|
+
const parsed = JSON.parse(trimmed) as { syntheticPaths?: unknown };
|
|
79
|
+
if (!Array.isArray(parsed.syntheticPaths)) return [];
|
|
80
|
+
return [...new Set(parsed.syntheticPaths.filter((entry): entry is string => typeof entry === "string").map((entry) => normalizeSyntheticPath(worktreePath, entry)))];
|
|
81
|
+
}
|
|
82
|
+
|
|
33
83
|
export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState): PreparedTaskWorkspace {
|
|
34
84
|
if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
|
|
35
85
|
const repoRoot = findGitRoot(manifest.cwd);
|
|
@@ -47,7 +97,28 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
|
|
|
47
97
|
return { cwd: worktreePath, worktreePath, branch, reused: true };
|
|
48
98
|
}
|
|
49
99
|
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
50
|
-
|
|
100
|
+
const syntheticPaths = runSetupHook(manifest, task, repoRoot, worktreePath, branch);
|
|
101
|
+
const nodeModulesLinked = loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
|
|
102
|
+
return { cwd: worktreePath, worktreePath, branch, reused: false, nodeModulesLinked, syntheticPaths };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function captureWorktreeDiffStat(worktreePath: string): WorktreeDiffStat {
|
|
106
|
+
try {
|
|
107
|
+
const diffStat = git(worktreePath, ["diff", "--stat"]);
|
|
108
|
+
const numstat = git(worktreePath, ["diff", "--numstat"]);
|
|
109
|
+
let filesChanged = 0;
|
|
110
|
+
let insertions = 0;
|
|
111
|
+
let deletions = 0;
|
|
112
|
+
for (const line of numstat.split(/\r?\n/).filter(Boolean)) {
|
|
113
|
+
const [add, del] = line.split(/\s+/);
|
|
114
|
+
filesChanged += 1;
|
|
115
|
+
insertions += Number(add) || 0;
|
|
116
|
+
deletions += Number(del) || 0;
|
|
117
|
+
}
|
|
118
|
+
return { filesChanged, insertions, deletions, diffStat };
|
|
119
|
+
} catch {
|
|
120
|
+
return { filesChanged: 0, insertions: 0, deletions: 0, diffStat: "" };
|
|
121
|
+
}
|
|
51
122
|
}
|
|
52
123
|
|
|
53
124
|
export function captureWorktreeDiff(worktreePath: string): string {
|