pi-crew 0.1.6 → 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 +3 -0
- package/src/agents/discover-agents.ts +34 -5
- package/src/config/config.ts +111 -11
- package/src/extension/async-notifier.ts +3 -1
- package/src/extension/cross-extension-rpc.ts +82 -0
- package/src/extension/register.ts +19 -3
- package/src/extension/result-watcher.ts +89 -0
- package/src/extension/team-tool.ts +145 -19
- package/src/prompt/prompt-runtime.ts +12 -2
- package/src/runtime/agent-memory.ts +72 -0
- package/src/runtime/agent-observability.ts +88 -0
- package/src/runtime/child-pi.ts +60 -4
- package/src/runtime/crew-agent-records.ts +42 -4
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/foreground-control.ts +82 -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/pi-args.ts +29 -0
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/sidechain-output.ts +28 -0
- package/src/runtime/task-runner.ts +83 -12
- package/src/runtime/team-runner.ts +4 -1
- package/src/state/event-log.ts +19 -0
- package/src/ui/run-dashboard.ts +82 -10
- package/src/utils/file-coalescer.ts +33 -0
- package/src/worktree/worktree-manager.ts +73 -2
package/src/runtime/pi-args.ts
CHANGED
|
@@ -7,12 +7,14 @@ import type { AgentConfig } from "../agents/agent-config.ts";
|
|
|
7
7
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
8
8
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompt", "prompt-runtime.ts");
|
|
9
9
|
const TASK_ARG_LIMIT = 8000;
|
|
10
|
+
const DEFAULT_MAX_CREW_DEPTH = 2;
|
|
10
11
|
|
|
11
12
|
export interface BuildPiWorkerArgsInput {
|
|
12
13
|
task: string;
|
|
13
14
|
agent: AgentConfig;
|
|
14
15
|
model?: string;
|
|
15
16
|
sessionEnabled?: boolean;
|
|
17
|
+
maxDepth?: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface BuildPiWorkerArgsResult {
|
|
@@ -28,6 +30,25 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
|
|
|
28
30
|
return `${model}:${thinking}`;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
export function currentCrewDepth(env: NodeJS.ProcessEnv = process.env): number {
|
|
34
|
+
const raw = env.PI_CREW_DEPTH ?? env.PI_TEAMS_DEPTH ?? "0";
|
|
35
|
+
const parsed = Number(raw);
|
|
36
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveCrewMaxDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): number {
|
|
40
|
+
const raw = env.PI_CREW_MAX_DEPTH ?? env.PI_TEAMS_MAX_DEPTH;
|
|
41
|
+
const envDepth = raw !== undefined ? Number(raw) : NaN;
|
|
42
|
+
if (Number.isInteger(envDepth) && envDepth >= 0) return envDepth;
|
|
43
|
+
return Number.isInteger(inputMaxDepth) && inputMaxDepth !== undefined && inputMaxDepth >= 0 ? inputMaxDepth : DEFAULT_MAX_CREW_DEPTH;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function checkCrewDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): { blocked: boolean; depth: number; maxDepth: number } {
|
|
47
|
+
const depth = currentCrewDepth(env);
|
|
48
|
+
const maxDepth = resolveCrewMaxDepth(inputMaxDepth, env);
|
|
49
|
+
return { depth, maxDepth, blocked: depth >= maxDepth };
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerArgsResult {
|
|
32
53
|
const args = ["--mode", "json", "-p"];
|
|
33
54
|
if (input.sessionEnabled === false) args.push("--no-session");
|
|
@@ -61,11 +82,19 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
61
82
|
args.push(`Task: ${input.task}`);
|
|
62
83
|
}
|
|
63
84
|
|
|
85
|
+
const parentDepth = currentCrewDepth();
|
|
86
|
+
const maxDepth = resolveCrewMaxDepth(input.maxDepth);
|
|
64
87
|
return {
|
|
65
88
|
args,
|
|
66
89
|
env: {
|
|
90
|
+
PI_CREW_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
91
|
+
PI_CREW_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
92
|
+
PI_CREW_DEPTH: String(parentDepth + 1),
|
|
93
|
+
PI_CREW_MAX_DEPTH: String(maxDepth),
|
|
67
94
|
PI_TEAMS_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
68
95
|
PI_TEAMS_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
96
|
+
PI_TEAMS_DEPTH: String(parentDepth + 1),
|
|
97
|
+
PI_TEAMS_MAX_DEPTH: String(maxDepth),
|
|
69
98
|
},
|
|
70
99
|
tempDir,
|
|
71
100
|
};
|
|
@@ -48,7 +48,7 @@ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJ
|
|
|
48
48
|
|
|
49
49
|
export async function resolveCrewRuntime(config: PiTeamsConfig, env: NodeJS.ProcessEnv = process.env): Promise<CrewRuntimeCapabilities> {
|
|
50
50
|
const requestedMode = config.runtime?.mode ?? "auto";
|
|
51
|
-
const executeWorkers = config.executeWorkers === true || env.PI_TEAMS_EXECUTE_WORKERS === "1";
|
|
51
|
+
const executeWorkers = config.executeWorkers === true || env.PI_CREW_EXECUTE_WORKERS === "1" || env.PI_TEAMS_EXECUTE_WORKERS === "1";
|
|
52
52
|
if (requestedMode === "scaffold") return scaffoldCaps(requestedMode);
|
|
53
53
|
if (requestedMode === "child-process") return childCaps(requestedMode, executeWorkers ? undefined : "child-process requested but executeWorkers is not enabled; caller should refuse or fall back explicitly.");
|
|
54
54
|
if (requestedMode === "live-session" || (requestedMode === "auto" && config.runtime?.preferLiveSession === true)) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SidechainEntry {
|
|
5
|
+
isSidechain: true;
|
|
6
|
+
agentId: string;
|
|
7
|
+
type: string;
|
|
8
|
+
message: unknown;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
19
|
+
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function eventToSidechainType(event: unknown): string | undefined {
|
|
23
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
24
|
+
const type = (event as { type?: unknown }).type;
|
|
25
|
+
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
26
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
27
|
+
return typeof type === "string" ? type : undefined;
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
3
|
+
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
3
4
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
4
5
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
@@ -7,7 +8,7 @@ import { saveRunManifest, saveRunTasks } from "../state/state-store.ts";
|
|
|
7
8
|
import { createTaskClaim } from "../state/task-claims.ts";
|
|
8
9
|
import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
|
|
9
10
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
10
|
-
import { captureWorktreeDiff, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
11
|
+
import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
11
12
|
import { buildModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
|
|
12
13
|
import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
|
|
13
14
|
import { runChildPi } from "./child-pi.ts";
|
|
@@ -18,7 +19,9 @@ import { permissionForRole } from "./role-permission.ts";
|
|
|
18
19
|
import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
|
|
19
20
|
import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
|
|
20
21
|
import { parseSessionUsage } from "./session-usage.ts";
|
|
21
|
-
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";
|
|
22
25
|
|
|
23
26
|
export interface TaskRunnerInput {
|
|
24
27
|
manifest: TeamRunManifest;
|
|
@@ -28,6 +31,12 @@ export interface TaskRunnerInput {
|
|
|
28
31
|
agent: AgentConfig;
|
|
29
32
|
signal?: AbortSignal;
|
|
30
33
|
executeWorkers: boolean;
|
|
34
|
+
runtimeKind?: CrewRuntimeKind;
|
|
35
|
+
runtimeConfig?: CrewRuntimeConfig;
|
|
36
|
+
parentContext?: string;
|
|
37
|
+
parentModel?: unknown;
|
|
38
|
+
modelRegistry?: unknown;
|
|
39
|
+
limits?: CrewLimitsConfig;
|
|
31
40
|
dependencyContextText?: string;
|
|
32
41
|
}
|
|
33
42
|
|
|
@@ -55,7 +64,8 @@ function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
|
55
64
|
].join("\n");
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
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"))) : "";
|
|
59
69
|
return [
|
|
60
70
|
"# pi-crew Worker Runtime Context",
|
|
61
71
|
`Run ID: ${manifest.runId}`,
|
|
@@ -86,6 +96,7 @@ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: T
|
|
|
86
96
|
task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
|
|
87
97
|
"",
|
|
88
98
|
(inputDependencyContext(task) || ""),
|
|
99
|
+
memoryBlock,
|
|
89
100
|
"Task:",
|
|
90
101
|
step.task.replaceAll("{goal}", manifest.goal),
|
|
91
102
|
].join("\n");
|
|
@@ -190,6 +201,9 @@ function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, st
|
|
|
190
201
|
next.currentToolArgs = undefined;
|
|
191
202
|
next.currentToolStartedAt = undefined;
|
|
192
203
|
}
|
|
204
|
+
if ((obj?.type === "tool_execution_error" || obj?.type === "tool_execution_failed") && next.currentTool) {
|
|
205
|
+
next.failedTool = next.currentTool;
|
|
206
|
+
}
|
|
193
207
|
const usage = eventUsage(event);
|
|
194
208
|
if (usage) {
|
|
195
209
|
next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
|
|
@@ -222,12 +236,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
222
236
|
...(dependencyContextText ? { dependencyContextText } : {}),
|
|
223
237
|
} as TeamTaskState;
|
|
224
238
|
let tasks = updateTask(input.tasks, task);
|
|
239
|
+
const runtimeKind = input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
225
240
|
saveRunTasks(manifest, tasks);
|
|
226
|
-
upsertCrewAgent(manifest, recordFromTask(manifest, task,
|
|
227
|
-
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 } });
|
|
228
243
|
const permissionMode = permissionForRole(task.role);
|
|
229
244
|
|
|
230
|
-
const prompt = renderTaskPrompt(manifest, input.step, task);
|
|
245
|
+
const prompt = renderTaskPrompt(manifest, input.step, task, input.agent);
|
|
231
246
|
const promptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
232
247
|
kind: "prompt",
|
|
233
248
|
relativePath: `prompts/${task.id}.md`,
|
|
@@ -243,7 +258,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
243
258
|
let modelAttempts: ModelAttemptSummary[] | undefined;
|
|
244
259
|
let parsedOutput: ParsedPiJsonOutput | undefined;
|
|
245
260
|
|
|
246
|
-
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 });
|
|
247
262
|
const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
|
|
248
263
|
const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
249
264
|
kind: "metadata",
|
|
@@ -251,7 +266,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
251
266
|
content: `${coordinationBridgeInstructions(task)}\n`,
|
|
252
267
|
producer: task.id,
|
|
253
268
|
});
|
|
254
|
-
if (
|
|
269
|
+
if (runtimeKind === "child-process") {
|
|
255
270
|
const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
|
|
256
271
|
const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
|
|
257
272
|
const logs: string[] = [];
|
|
@@ -269,6 +284,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
269
284
|
model,
|
|
270
285
|
signal: input.signal,
|
|
271
286
|
transcriptPath,
|
|
287
|
+
maxDepth: input.limits?.maxTaskDepth,
|
|
272
288
|
onStdoutLine: (line) => appendCrewAgentOutput(manifest, task.id, line),
|
|
273
289
|
onJsonEvent: (event) => {
|
|
274
290
|
appendCrewAgentEvent(manifest, task.id, event);
|
|
@@ -320,6 +336,55 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
320
336
|
producer: task.id,
|
|
321
337
|
});
|
|
322
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
|
+
}
|
|
323
388
|
} else {
|
|
324
389
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
325
390
|
kind: "result",
|
|
@@ -340,6 +405,12 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
340
405
|
content: captureWorktreeDiff(workspace.worktreePath),
|
|
341
406
|
producer: task.id,
|
|
342
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;
|
|
343
414
|
|
|
344
415
|
task = {
|
|
345
416
|
...task,
|
|
@@ -349,9 +420,9 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
349
420
|
modelAttempts,
|
|
350
421
|
usage: parsedOutput?.usage,
|
|
351
422
|
jsonEvents: parsedOutput?.jsonEvents,
|
|
352
|
-
agentProgress: task.agentProgress,
|
|
423
|
+
agentProgress: error && task.agentProgress?.currentTool ? { ...task.agentProgress, failedTool: task.agentProgress.currentTool } : task.agentProgress,
|
|
353
424
|
error,
|
|
354
|
-
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.`),
|
|
355
426
|
promptArtifact,
|
|
356
427
|
resultArtifact,
|
|
357
428
|
claim: undefined,
|
|
@@ -385,10 +456,10 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
385
456
|
content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
|
|
386
457
|
producer: task.id,
|
|
387
458
|
});
|
|
388
|
-
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] : [])] };
|
|
389
460
|
saveRunManifest(manifest);
|
|
390
461
|
saveRunTasks(manifest, tasks);
|
|
391
|
-
upsertCrewAgent(manifest, recordFromTask(manifest, task,
|
|
462
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
|
|
392
463
|
appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
|
|
393
464
|
return { manifest, tasks };
|
|
394
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 });
|
|
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);
|
package/src/state/event-log.ts
CHANGED
|
@@ -94,6 +94,25 @@ export function readEvents(eventsPath: string): TeamEvent[] {
|
|
|
94
94
|
.map((line) => JSON.parse(line) as TeamEvent);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export interface EventCursorOptions {
|
|
98
|
+
sinceSeq?: number;
|
|
99
|
+
limit?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function positiveInteger(value: number | undefined): number | undefined {
|
|
103
|
+
return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): { events: TeamEvent[]; nextSeq: number; total: number } {
|
|
107
|
+
const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
|
|
108
|
+
const limit = positiveInteger(options.limit);
|
|
109
|
+
const all = readEvents(eventsPath);
|
|
110
|
+
const filtered = all.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
|
|
111
|
+
const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
|
|
112
|
+
const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
|
|
113
|
+
return { events, nextSeq: returnedMaxSeq, total: filtered.length };
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
export function dedupeTerminalEvents(events: TeamEvent[]): TeamEvent[] {
|
|
98
117
|
const seen = new Set<string>();
|
|
99
118
|
const output: TeamEvent[] = [];
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
|
+
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
3
5
|
|
|
4
6
|
interface DashboardComponent {
|
|
5
7
|
invalidate(): void;
|
|
@@ -7,17 +9,43 @@ interface DashboardComponent {
|
|
|
7
9
|
handleInput(data: string): void;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "reload";
|
|
12
|
+
export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "events" | "agents" | "agent-events" | "agent-output" | "agent-transcript" | "reload";
|
|
11
13
|
export interface RunDashboardSelection {
|
|
12
14
|
runId: string;
|
|
13
15
|
action: RunDashboardAction;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
19
|
+
|
|
20
|
+
function visibleLength(value: string): number {
|
|
21
|
+
return value.replace(ANSI_PATTERN, "").length;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
function truncate(value: string, width: number): string {
|
|
17
25
|
if (width <= 0) return "";
|
|
18
|
-
if (value
|
|
26
|
+
if (visibleLength(value) <= width) return value;
|
|
19
27
|
if (width <= 1) return "…";
|
|
20
|
-
|
|
28
|
+
let output = "";
|
|
29
|
+
let visible = 0;
|
|
30
|
+
for (let index = 0; index < value.length;) {
|
|
31
|
+
const slice = value.slice(index);
|
|
32
|
+
const ansi = slice.match(/^\u001b\[[0-?]*[ -/]*[@-~]/);
|
|
33
|
+
if (ansi?.[0]) {
|
|
34
|
+
output += ansi[0];
|
|
35
|
+
index += ansi[0].length;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const char = value[index]!;
|
|
39
|
+
if (visible >= width - 1) break;
|
|
40
|
+
output += char;
|
|
41
|
+
visible += 1;
|
|
42
|
+
index += char.length;
|
|
43
|
+
}
|
|
44
|
+
return `${output}\u001b[0m…`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function padVisible(value: string, width: number): string {
|
|
48
|
+
return `${value}${" ".repeat(Math.max(0, width - visibleLength(value)))}`;
|
|
21
49
|
}
|
|
22
50
|
|
|
23
51
|
function statusIcon(status: string): string {
|
|
@@ -40,6 +68,30 @@ function readProgressPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
70
|
|
|
71
|
+
function agentPreviewLine(agent: CrewAgentRecord): string {
|
|
72
|
+
const stats = [
|
|
73
|
+
agent.progress?.activityState,
|
|
74
|
+
agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
|
|
75
|
+
agent.toolUses !== undefined ? `${agent.toolUses} tools` : undefined,
|
|
76
|
+
agent.progress?.tokens !== undefined ? `${agent.progress.tokens} tok` : undefined,
|
|
77
|
+
agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
|
|
78
|
+
agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
|
|
79
|
+
].filter((part): part is string => Boolean(part));
|
|
80
|
+
const recent = agent.progress?.recentOutput?.at(-1);
|
|
81
|
+
return `Agent: ${statusIcon(agent.status)} ${agent.taskId} ${agent.role}->${agent.agent}${stats.length ? ` · ${stats.join(" · ")}` : ""}${recent ? ` ⎿ ${recent}` : ""}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readAgentPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
85
|
+
try {
|
|
86
|
+
const agents = readCrewAgents(run);
|
|
87
|
+
if (!agents.length) return ["Agents: (none)"];
|
|
88
|
+
return ["Agents:", ...agents.slice(0, maxLines).map(agentPreviewLine), ...(agents.length > maxLines ? [`Agents: +${agents.length - maxLines} more`] : [])];
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
return [`Agents: failed to read (${message})`];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
function countByStatus(runs: TeamRunManifest[]): string {
|
|
44
96
|
const counts = new Map<string, number>();
|
|
45
97
|
for (const run of runs) counts.set(run.status, (counts.get(run.status) ?? 0) + 1);
|
|
@@ -64,19 +116,19 @@ export class RunDashboard implements DashboardComponent {
|
|
|
64
116
|
const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
|
|
65
117
|
const lines = [
|
|
66
118
|
`╭${"─".repeat(borderWidth)}╮`,
|
|
67
|
-
`│ ${truncate("pi-crew dashboard", innerWidth - 1)
|
|
68
|
-
`│ ${truncate("↑/↓/j/k select • r reload • p progress • s/u/a/i actions • q close", innerWidth - 1)
|
|
69
|
-
`│ ${truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1)
|
|
119
|
+
`│ ${padVisible(truncate("pi-crew dashboard", innerWidth - 1), innerWidth - 1)}│`,
|
|
120
|
+
`│ ${padVisible(truncate("↑/↓/j/k select • r reload • p progress • s/u/a/i actions • d agents • e/v/o viewers • q close", innerWidth - 1), innerWidth - 1)}│`,
|
|
121
|
+
`│ ${padVisible(truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1), innerWidth - 1)}│`,
|
|
70
122
|
`├${"─".repeat(borderWidth)}┤`,
|
|
71
123
|
];
|
|
72
124
|
if (this.runs.length === 0) {
|
|
73
|
-
lines.push(`│ ${truncate("No runs found.", innerWidth - 1)
|
|
125
|
+
lines.push(`│ ${padVisible(truncate("No runs found.", innerWidth - 1), innerWidth - 1)}│`);
|
|
74
126
|
} else {
|
|
75
127
|
for (let i = 0; i < Math.min(this.runs.length, 10); i++) {
|
|
76
128
|
const run = this.runs[i]!;
|
|
77
129
|
const marker = i === this.selected ? "›" : " ";
|
|
78
130
|
const text = `${marker} ${statusIcon(run.status)} ${run.runId} ${run.status} ${run.team}/${run.workflow ?? "none"} ${run.goal}`;
|
|
79
|
-
lines.push(`│ ${truncate(text, innerWidth - 1)
|
|
131
|
+
lines.push(`│ ${padVisible(truncate(text, innerWidth - 1), innerWidth - 1)}│`);
|
|
80
132
|
}
|
|
81
133
|
const selectedRun = this.runs[this.selected];
|
|
82
134
|
if (selectedRun) {
|
|
@@ -90,8 +142,8 @@ export class RunDashboard implements DashboardComponent {
|
|
|
90
142
|
selectedRun.async ? `Async: pid=${selectedRun.async.pid ?? "unknown"} log=${selectedRun.async.logPath}` : "Async: no",
|
|
91
143
|
`Goal: ${selectedRun.goal}`,
|
|
92
144
|
];
|
|
93
|
-
for (const detail of [...details, ...readProgressPreview(selectedRun, this.showFullProgress ? 20 : 5)]) {
|
|
94
|
-
lines.push(`│ ${truncate(detail, innerWidth - 1)
|
|
145
|
+
for (const detail of [...details, ...readAgentPreview(selectedRun), ...readProgressPreview(selectedRun, this.showFullProgress ? 20 : 5)]) {
|
|
146
|
+
lines.push(`│ ${padVisible(truncate(detail, innerWidth - 1), innerWidth - 1)}│`);
|
|
95
147
|
}
|
|
96
148
|
}
|
|
97
149
|
}
|
|
@@ -124,6 +176,26 @@ export class RunDashboard implements DashboardComponent {
|
|
|
124
176
|
this.done(runId ? { runId, action: "api" } : undefined);
|
|
125
177
|
return;
|
|
126
178
|
}
|
|
179
|
+
if (data === "d") {
|
|
180
|
+
const runId = this.runs[this.selected]?.runId;
|
|
181
|
+
this.done(runId ? { runId, action: "agents" } : undefined);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (data === "e") {
|
|
185
|
+
const runId = this.runs[this.selected]?.runId;
|
|
186
|
+
this.done(runId ? { runId, action: "agent-events" } : undefined);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (data === "o") {
|
|
190
|
+
const runId = this.runs[this.selected]?.runId;
|
|
191
|
+
this.done(runId ? { runId, action: "agent-output" } : undefined);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (data === "v") {
|
|
195
|
+
const runId = this.runs[this.selected]?.runId;
|
|
196
|
+
this.done(runId ? { runId, action: "agent-transcript" } : undefined);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
127
199
|
if (data === "r") {
|
|
128
200
|
this.done({ runId: this.runs[this.selected]?.runId ?? "", action: "reload" });
|
|
129
201
|
return;
|
|
@@ -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 {
|