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.
@@ -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, input.executeWorkers ? "child-process" : "scaffold"));
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: input.executeWorkers ? "pi" : "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 });
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 (input.executeWorkers) {
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}` : input.executeWorkers ? "Worker finished without reporting a verification failure." : "Safe scaffold mode; verification commands were not executed."),
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, input.executeWorkers ? "child-process" : "scaffold"));
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
- return { cwd: worktreePath, worktreePath, branch, reused: false };
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 {