pi-crew 0.1.4 → 0.1.6

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.
@@ -0,0 +1,107 @@
1
+ import type { TaskGraphNode, TeamTaskState } from "../state/types.ts";
2
+
3
+ export interface TaskGraphSchedulerSnapshot {
4
+ ready: string[];
5
+ blocked: string[];
6
+ running: string[];
7
+ done: string[];
8
+ failed: string[];
9
+ cancelled: string[];
10
+ }
11
+
12
+ function completedStepIds(tasks: TeamTaskState[]): Set<string> {
13
+ return new Set(tasks.filter((task) => task.status === "completed").map((task) => task.stepId).filter((id): id is string => id !== undefined));
14
+ }
15
+
16
+ function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
17
+ return new Map(tasks.map((task) => [task.id, task]));
18
+ }
19
+
20
+ function stepIdToTaskId(tasks: TeamTaskState[]): Map<string, string> {
21
+ return new Map(tasks.map((task) => [task.stepId, task.id]).filter((entry): entry is [string, string] => entry[0] !== undefined));
22
+ }
23
+
24
+ function dependencySatisfied(task: TeamTaskState, doneStepIds: Set<string>, idMap: Map<string, TeamTaskState>, stepMap: Map<string, string>): boolean {
25
+ return task.dependsOn.every((dependency) => {
26
+ if (doneStepIds.has(dependency)) return true;
27
+ const taskId = stepMap.get(dependency) ?? dependency;
28
+ return idMap.get(taskId)?.status === "completed";
29
+ });
30
+ }
31
+
32
+ function withQueue(task: TeamTaskState, queue: TaskGraphNode["queue"]): TeamTaskState {
33
+ return task.graph ? { ...task, graph: { ...task.graph, queue } } : task;
34
+ }
35
+
36
+ export function refreshTaskGraphQueues(tasks: TeamTaskState[]): TeamTaskState[] {
37
+ const doneSteps = completedStepIds(tasks);
38
+ const ids = taskById(tasks);
39
+ const steps = stepIdToTaskId(tasks);
40
+ return tasks.map((task) => {
41
+ if (task.status === "queued") return withQueue(task, dependencySatisfied(task, doneSteps, ids, steps) ? "ready" : "blocked");
42
+ if (task.status === "running") return withQueue(task, "running");
43
+ if (task.status === "completed" || task.status === "skipped") return withQueue(task, "done");
44
+ return withQueue(task, "blocked");
45
+ });
46
+ }
47
+
48
+ export function getReadyTasks(tasks: TeamTaskState[], maxCount = 1): TeamTaskState[] {
49
+ return refreshTaskGraphQueues(tasks).filter((task) => task.status === "queued" && task.graph?.queue === "ready").slice(0, Math.max(0, maxCount));
50
+ }
51
+
52
+ export function markTaskRunning(tasks: TeamTaskState[], taskId: string, now = new Date()): TeamTaskState[] {
53
+ return refreshTaskGraphQueues(tasks).map((task) => task.id === taskId ? withQueue({ ...task, status: "running", startedAt: task.startedAt ?? now.toISOString() }, "running") : task);
54
+ }
55
+
56
+ export function markTaskDone(tasks: TeamTaskState[], taskId: string, now = new Date()): TeamTaskState[] {
57
+ return refreshTaskGraphQueues(tasks.map((task) => task.id === taskId ? { ...task, status: "completed", finishedAt: task.finishedAt ?? now.toISOString() } : task));
58
+ }
59
+
60
+ export function cancelTaskSubtree(tasks: TeamTaskState[], rootTaskId: string, reason = "Cancelled by task graph scheduler.", now = new Date()): TeamTaskState[] {
61
+ const ids = taskById(tasks);
62
+ const toCancel = new Set<string>();
63
+ const stack = [rootTaskId];
64
+ while (stack.length) {
65
+ const current = stack.pop();
66
+ if (!current || toCancel.has(current)) continue;
67
+ toCancel.add(current);
68
+ const task = ids.get(current);
69
+ for (const child of task?.graph?.children ?? []) stack.push(child);
70
+ }
71
+ return refreshTaskGraphQueues(tasks.map((task) => {
72
+ if (!toCancel.has(task.id)) return task;
73
+ if (task.status === "completed") return task;
74
+ return { ...task, status: "cancelled", error: reason, finishedAt: task.finishedAt ?? now.toISOString() };
75
+ }));
76
+ }
77
+
78
+ export function failTaskAndBlockChildren(tasks: TeamTaskState[], rootTaskId: string, reason: string, now = new Date()): TeamTaskState[] {
79
+ const ids = taskById(tasks);
80
+ const blocked = new Set<string>();
81
+ const root = ids.get(rootTaskId);
82
+ const stack = [...(root?.graph?.children ?? [])];
83
+ while (stack.length) {
84
+ const current = stack.pop();
85
+ if (!current || blocked.has(current)) continue;
86
+ blocked.add(current);
87
+ const task = ids.get(current);
88
+ for (const child of task?.graph?.children ?? []) stack.push(child);
89
+ }
90
+ return refreshTaskGraphQueues(tasks.map((task) => {
91
+ if (task.id === rootTaskId) return { ...task, status: "failed", error: reason, finishedAt: task.finishedAt ?? now.toISOString() };
92
+ if (blocked.has(task.id) && task.status === "queued") return { ...task, status: "skipped", error: `Blocked by failed task '${rootTaskId}'.`, finishedAt: task.finishedAt ?? now.toISOString() };
93
+ return task;
94
+ }));
95
+ }
96
+
97
+ export function taskGraphSnapshot(tasks: TeamTaskState[]): TaskGraphSchedulerSnapshot {
98
+ const refreshed = refreshTaskGraphQueues(tasks);
99
+ return {
100
+ ready: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "ready").map((task) => task.id),
101
+ blocked: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "blocked").map((task) => task.id),
102
+ running: refreshed.filter((task) => task.status === "running").map((task) => task.id),
103
+ done: refreshed.filter((task) => task.status === "completed" || task.status === "skipped").map((task) => task.id),
104
+ failed: refreshed.filter((task) => task.status === "failed").map((task) => task.id),
105
+ cancelled: refreshed.filter((task) => task.status === "cancelled").map((task) => task.id),
106
+ };
107
+ }
@@ -0,0 +1,106 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
+ import { writeArtifact } from "../state/artifact-store.ts";
5
+ import type { WorkflowStep } from "../workflows/workflow-config.ts";
6
+
7
+ export interface DependencyOutputContext {
8
+ dependencies: Array<{ taskId: string; title: string; status: string; result?: string; resultPath?: string }>;
9
+ sharedReads: Array<{ name: string; path: string; content: string }>;
10
+ }
11
+
12
+ function readIfSmall(filePath: string, maxBytes = 24_000): string | undefined {
13
+ try {
14
+ const stat = fs.statSync(filePath);
15
+ if (stat.size > maxBytes) return `${fs.readFileSync(filePath, "utf-8").slice(0, maxBytes)}\n\n...(truncated ${stat.size - maxBytes} bytes)`;
16
+ return fs.readFileSync(filePath, "utf-8");
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
22
+ export function sharedPath(manifest: TeamRunManifest, name: string): string {
23
+ return path.join(manifest.artifactsRoot, "shared", name);
24
+ }
25
+
26
+ export function collectDependencyOutputContext(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, step: WorkflowStep): DependencyOutputContext {
27
+ const byStep = new Map(tasks.map((item) => [item.stepId, item]).filter((entry): entry is [string, TeamTaskState] => Boolean(entry[0])));
28
+ const byId = new Map(tasks.map((item) => [item.id, item]));
29
+ const dependencies = task.dependsOn.map((dep) => byStep.get(dep) ?? byId.get(dep)).filter((item): item is TeamTaskState => Boolean(item)).map((item) => ({
30
+ taskId: item.id,
31
+ title: item.title,
32
+ status: item.status,
33
+ resultPath: item.resultArtifact?.path,
34
+ result: item.resultArtifact ? readIfSmall(item.resultArtifact.path) : undefined,
35
+ }));
36
+ const sharedReads = (step.reads === false ? [] : step.reads ?? []).map((name) => {
37
+ const filePath = sharedPath(manifest, name);
38
+ return { name, path: filePath, content: readIfSmall(filePath) ?? "" };
39
+ }).filter((item) => item.content.trim().length > 0);
40
+ return { dependencies, sharedReads };
41
+ }
42
+
43
+ export function renderDependencyOutputContext(context: DependencyOutputContext): string {
44
+ const parts: string[] = [];
45
+ if (context.dependencies.length) {
46
+ parts.push("# Dependency Outputs", "");
47
+ for (const dep of context.dependencies) {
48
+ parts.push(`## ${dep.taskId} (${dep.title})`, `Status: ${dep.status}`, dep.resultPath ? `Result artifact: ${dep.resultPath}` : "", "", dep.result?.trim() || "(no result output)", "");
49
+ }
50
+ }
51
+ if (context.sharedReads.length) {
52
+ parts.push("# Shared Run Context Reads", "");
53
+ for (const read of context.sharedReads) parts.push(`## shared/${read.name}`, `Path: ${read.path}`, "", read.content.trim(), "");
54
+ }
55
+ return parts.join("\n").trim();
56
+ }
57
+
58
+ export function writeTaskSharedOutput(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): ArtifactDescriptor | undefined {
59
+ if (step.output === false) return undefined;
60
+ const name = step.output || `${task.id}.md`;
61
+ const source = task.resultArtifact ? readIfSmall(task.resultArtifact.path, 80_000) : undefined;
62
+ if (!source) return undefined;
63
+ return writeArtifact(manifest.artifactsRoot, {
64
+ kind: "metadata",
65
+ relativePath: `shared/${name}`,
66
+ producer: task.id,
67
+ content: source.endsWith("\n") ? source : `${source}\n`,
68
+ });
69
+ }
70
+
71
+ export function writeTaskInputsArtifact(manifest: TeamRunManifest, task: TeamTaskState, context: DependencyOutputContext): ArtifactDescriptor {
72
+ return writeArtifact(manifest.artifactsRoot, {
73
+ kind: "metadata",
74
+ relativePath: `metadata/${task.id}.inputs.json`,
75
+ producer: task.id,
76
+ content: `${JSON.stringify(context, null, 2)}\n`,
77
+ });
78
+ }
79
+
80
+ export function aggregateTaskOutputs(tasks: TeamTaskState[]): string {
81
+ return tasks.map((task, index) => {
82
+ const body = task.resultArtifact ? readIfSmall(task.resultArtifact.path, 40_000) : undefined;
83
+ const hasBody = Boolean(body?.trim());
84
+ const expectedMissing = task.resultArtifact && !fs.existsSync(task.resultArtifact.path);
85
+ const status = task.status === "skipped"
86
+ ? "SKIPPED"
87
+ : task.status === "failed"
88
+ ? `FAILED${task.exitCode !== undefined ? ` (exit code ${task.exitCode ?? "null"})` : ""}${task.error ? `: ${task.error}` : ""}`
89
+ : expectedMissing
90
+ ? `EMPTY OUTPUT (expected result artifact missing: ${task.resultArtifact?.path})`
91
+ : !hasBody
92
+ ? "EMPTY OUTPUT (no textual response returned)"
93
+ : task.status.toUpperCase();
94
+ return [
95
+ `=== Task ${index + 1}: ${task.id} (${task.agent}) ===`,
96
+ `Status: ${status}`,
97
+ task.role ? `Role: ${task.role}` : "",
98
+ task.resultArtifact?.path ? `Result artifact: ${task.resultArtifact.path}` : "",
99
+ task.logArtifact?.path ? `Log artifact: ${task.logArtifact.path}` : "",
100
+ task.transcriptArtifact?.path ? `Transcript: ${task.transcriptArtifact.path}` : "",
101
+ task.usage ? `Usage: ${JSON.stringify(task.usage)}` : "",
102
+ "",
103
+ hasBody ? body!.trim() : status,
104
+ ].filter(Boolean).join("\n");
105
+ }).join("\n\n");
106
+ }
@@ -1,5 +1,6 @@
1
+ import * as fs from "node:fs";
1
2
  import type { AgentConfig } from "../agents/agent-config.ts";
2
- import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
3
4
  import { writeArtifact } from "../state/artifact-store.ts";
4
5
  import { appendEvent } from "../state/event-log.ts";
5
6
  import { saveRunManifest, saveRunTasks } from "../state/state-store.ts";
@@ -12,6 +13,12 @@ import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts"
12
13
  import { runChildPi } from "./child-pi.ts";
13
14
  import { buildTaskPacket, renderTaskPacket } from "./task-packet.ts";
14
15
  import { createVerificationEvidence } from "./green-contract.ts";
16
+ import { createStartupEvidence } from "./worker-startup.ts";
17
+ import { permissionForRole } from "./role-permission.ts";
18
+ import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
19
+ import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
20
+ import { parseSessionUsage } from "./session-usage.ts";
21
+ import type { CrewAgentProgress } from "./crew-agent-runtime.ts";
15
22
 
16
23
  export interface TaskRunnerInput {
17
24
  manifest: TeamRunManifest;
@@ -21,6 +28,31 @@ export interface TaskRunnerInput {
21
28
  agent: AgentConfig;
22
29
  signal?: AbortSignal;
23
30
  executeWorkers: boolean;
31
+ dependencyContextText?: string;
32
+ }
33
+
34
+ function readOnlyRoleInstructions(role: string): string {
35
+ if (permissionForRole(role) !== "read_only") return "";
36
+ return [
37
+ "# READ-ONLY ROLE CONTRACT",
38
+ "You are running in READ-ONLY mode for this task.",
39
+ "- Do not create, modify, delete, move, or copy files.",
40
+ "- Do not use shell redirects, heredocs, in-place edits, package installs, git commit/merge/rebase/reset/checkout, or other state-mutating commands.",
41
+ "- If implementation changes are needed, report exact recommendations instead of applying them.",
42
+ "- Prefer read/grep/find/listing tools and read-only git inspection commands.",
43
+ ].join("\n");
44
+ }
45
+
46
+ function coordinationBridgeInstructions(task: TeamTaskState): string {
47
+ return [
48
+ "# Crew Coordination Channel",
49
+ `Mailbox target for this task: ${task.id}`,
50
+ "Use the run mailbox contract for coordination with the leader/orchestrator:",
51
+ "- If blocked or uncertain, report the blocker in your final result and, when mailbox tools/API are available, send an inbox/outbox message addressed to the leader.",
52
+ "- If nudged, answer with current status, blocker, or smallest next step.",
53
+ "- Treat inherited/dependency context as reference-only; do not continue the parent conversation directly.",
54
+ "- Completion handoff should include: DONE/FAILED, summary, changed/read files, verification evidence, and remaining risks.",
55
+ ].join("\n");
24
56
  }
25
57
 
26
58
  function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): string {
@@ -47,21 +79,136 @@ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: T
47
79
  "- Do not claim completion without evidence.",
48
80
  "- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
49
81
  "",
82
+ readOnlyRoleInstructions(task.role),
83
+ "",
84
+ coordinationBridgeInstructions(task),
85
+ "",
50
86
  task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
87
+ "",
88
+ (inputDependencyContext(task) || ""),
51
89
  "Task:",
52
90
  step.task.replaceAll("{goal}", manifest.goal),
53
91
  ].join("\n");
54
92
  }
55
93
 
94
+ function inputDependencyContext(task: TeamTaskState): string {
95
+ return (task as TeamTaskState & { dependencyContextText?: string }).dependencyContextText ?? "";
96
+ }
97
+
56
98
  function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
57
99
  return tasks.map((task) => task.id === updated.id ? updated : task);
58
100
  }
59
101
 
102
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
103
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
104
+ }
105
+
106
+ function textFromContent(content: unknown): string[] {
107
+ if (typeof content === "string") return [content];
108
+ if (!Array.isArray(content)) return [];
109
+ const text: string[] = [];
110
+ for (const part of content) {
111
+ const obj = asRecord(part);
112
+ if (!obj) continue;
113
+ if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
114
+ else if (typeof obj.content === "string") text.push(obj.content);
115
+ }
116
+ return text;
117
+ }
118
+
119
+ function eventText(event: unknown): string[] {
120
+ const obj = asRecord(event);
121
+ if (!obj) return [];
122
+ const text: string[] = [];
123
+ if (typeof obj.text === "string") text.push(obj.text);
124
+ if (typeof obj.output === "string") text.push(obj.output);
125
+ text.push(...textFromContent(obj.content));
126
+ const message = asRecord(obj.message);
127
+ if (message) text.push(...textFromContent(message.content));
128
+ return text.filter((entry) => entry.trim());
129
+ }
130
+
131
+ function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
132
+ for (const key of keys) {
133
+ const value = obj[key];
134
+ if (typeof value === "number" && Number.isFinite(value)) return value;
135
+ }
136
+ return undefined;
137
+ }
138
+
139
+ function eventUsage(event: unknown): { input?: number; output?: number; turns?: number } | undefined {
140
+ const obj = asRecord(event);
141
+ if (!obj) return undefined;
142
+ const direct = {
143
+ input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
144
+ output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
145
+ turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
146
+ };
147
+ if (Object.values(direct).some((value) => value !== undefined)) return direct;
148
+ for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
149
+ const nested = eventUsage(obj[key]);
150
+ if (nested) return nested;
151
+ }
152
+ const message = asRecord(obj.message);
153
+ return message ? eventUsage(message.usage) : undefined;
154
+ }
155
+
156
+ function previewArgs(args: unknown): string | undefined {
157
+ if (!args) return undefined;
158
+ try {
159
+ const text = typeof args === "string" ? args : JSON.stringify(args);
160
+ return text.length > 240 ? `${text.slice(0, 240)}…` : text;
161
+ } catch {
162
+ return undefined;
163
+ }
164
+ }
165
+
166
+ function applyUsageToProgress(progress: CrewAgentProgress | undefined, usage: UsageState | undefined): CrewAgentProgress | undefined {
167
+ if (!usage) return progress;
168
+ const base = progress ?? emptyCrewAgentProgress();
169
+ return {
170
+ ...base,
171
+ tokens: (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0),
172
+ turns: usage.turns ?? base.turns,
173
+ };
174
+ }
175
+
176
+ function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress {
177
+ const obj = asRecord(event);
178
+ const now = new Date().toISOString();
179
+ const next: CrewAgentProgress = { ...progress, recentTools: [...progress.recentTools], recentOutput: [...progress.recentOutput], lastActivityAt: now, activityState: "active" };
180
+ if (startedAt) next.durationMs = Date.now() - new Date(startedAt).getTime();
181
+ if (obj?.type === "tool_execution_start") {
182
+ next.toolCount += 1;
183
+ next.currentTool = typeof obj.toolName === "string" ? obj.toolName : typeof obj.name === "string" ? obj.name : "tool";
184
+ next.currentToolArgs = previewArgs(obj.args);
185
+ next.currentToolStartedAt = now;
186
+ }
187
+ if (obj?.type === "tool_execution_end") {
188
+ if (next.currentTool) next.recentTools.push({ tool: next.currentTool, args: next.currentToolArgs, endedAt: now });
189
+ next.currentTool = undefined;
190
+ next.currentToolArgs = undefined;
191
+ next.currentToolStartedAt = undefined;
192
+ }
193
+ const usage = eventUsage(event);
194
+ if (usage) {
195
+ next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
196
+ next.turns = usage.turns ?? next.turns;
197
+ }
198
+ const text = eventText(event);
199
+ if (text.length > 0) next.recentOutput.push(...text.flatMap((entry) => entry.split(/\r?\n/)).filter(Boolean).slice(-10));
200
+ if (next.recentTools.length > 25) next.recentTools.splice(0, next.recentTools.length - 25);
201
+ if (next.recentOutput.length > 50) next.recentOutput.splice(0, next.recentOutput.length - 50);
202
+ return next;
203
+ }
204
+
60
205
  export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
61
206
  let manifest = input.manifest;
62
207
  const workspace = prepareTaskWorkspace(manifest, input.task);
63
208
  const worktree = workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree;
64
209
  const taskPacket = buildTaskPacket({ manifest, step: input.step, taskId: input.task.id, cwd: workspace.cwd, worktreePath: worktree?.path });
210
+ const dependencyContext = collectDependencyOutputContext(manifest, input.tasks, input.task, input.step);
211
+ const dependencyContextText = input.dependencyContextText ?? renderDependencyOutputContext(dependencyContext);
65
212
  let task: TeamTaskState = {
66
213
  ...input.task,
67
214
  cwd: workspace.cwd,
@@ -71,10 +218,14 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
71
218
  startedAt: new Date().toISOString(),
72
219
  claim: createTaskClaim(`task-runner:${input.task.id}`),
73
220
  heartbeat: createWorkerHeartbeat(input.task.id),
74
- };
221
+ agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
222
+ ...(dependencyContextText ? { dependencyContextText } : {}),
223
+ } as TeamTaskState;
75
224
  let tasks = updateTask(input.tasks, task);
76
225
  saveRunTasks(manifest, tasks);
226
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, input.executeWorkers ? "child-process" : "scaffold"));
77
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 } });
228
+ const permissionMode = permissionForRole(task.role);
78
229
 
79
230
  const prompt = renderTaskPrompt(manifest, input.step, task);
80
231
  const promptArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -86,11 +237,20 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
86
237
 
87
238
  let resultArtifact: ArtifactDescriptor;
88
239
  let logArtifact: ArtifactDescriptor | undefined;
240
+ let transcriptArtifact: ArtifactDescriptor | undefined;
89
241
  let exitCode: number | null = 0;
90
242
  let error: string | undefined;
91
243
  let modelAttempts: ModelAttemptSummary[] | undefined;
92
244
  let parsedOutput: ParsedPiJsonOutput | undefined;
93
245
 
246
+ 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 });
247
+ const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
248
+ const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
249
+ kind: "metadata",
250
+ relativePath: `metadata/${task.id}.coordination-bridge.md`,
251
+ content: `${coordinationBridgeInstructions(task)}\n`,
252
+ producer: task.id,
253
+ });
94
254
  if (input.executeWorkers) {
95
255
  const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
96
256
  const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
@@ -98,9 +258,27 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
98
258
  let finalStdout = "";
99
259
  let finalStderr = "";
100
260
  modelAttempts = [];
261
+ const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
101
262
  for (let i = 0; i < attemptModels.length; i++) {
102
263
  const model = attemptModels[i];
103
- const childResult = await runChildPi({ cwd: task.cwd, task: prompt, agent: input.agent, model, signal: input.signal });
264
+ const attemptStartedAt = new Date();
265
+ const childResult = await runChildPi({
266
+ cwd: task.cwd,
267
+ task: prompt,
268
+ agent: input.agent,
269
+ model,
270
+ signal: input.signal,
271
+ transcriptPath,
272
+ onStdoutLine: (line) => appendCrewAgentOutput(manifest, task.id, line),
273
+ onJsonEvent: (event) => {
274
+ appendCrewAgentEvent(manifest, task.id, event);
275
+ task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
276
+ tasks = updateTask(tasks, task);
277
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
278
+ appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { event } });
279
+ },
280
+ });
281
+ startupEvidence = createStartupEvidence({ command: "pi", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: childResult.exitCode === 0 && !childResult.error, stderr: childResult.stderr, error: childResult.error, exitCode: childResult.exitCode });
104
282
  exitCode = childResult.exitCode;
105
283
  finalStdout = childResult.stdout;
106
284
  finalStderr = childResult.stderr;
@@ -126,6 +304,22 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
126
304
  content: [...logs, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`, parsedOutput?.usage ? `usage=${JSON.stringify(parsedOutput.usage)}` : "", "", "STDOUT:", finalStdout, "", "STDERR:", finalStderr].join("\n"),
127
305
  producer: task.id,
128
306
  });
307
+ const sessionUsage = parseSessionUsage(transcriptPath);
308
+ const effectiveUsage = parsedOutput?.usage ?? sessionUsage;
309
+ if (effectiveUsage) {
310
+ parsedOutput = { ...(parsedOutput ?? { jsonEvents: 0, textEvents: [] }), usage: effectiveUsage };
311
+ task = { ...task, usage: effectiveUsage, agentProgress: applyUsageToProgress(task.agentProgress, effectiveUsage) };
312
+ tasks = updateTask(tasks, task);
313
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
314
+ }
315
+ if (fs.existsSync(transcriptPath)) {
316
+ transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
317
+ kind: "log",
318
+ relativePath: `transcripts/${task.id}.jsonl`,
319
+ content: fs.readFileSync(transcriptPath, "utf-8"),
320
+ producer: task.id,
321
+ });
322
+ }
129
323
  } else {
130
324
  resultArtifact = writeArtifact(manifest.artifactsRoot, {
131
325
  kind: "result",
@@ -155,6 +349,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
155
349
  modelAttempts,
156
350
  usage: parsedOutput?.usage,
157
351
  jsonEvents: parsedOutput?.jsonEvents,
352
+ agentProgress: task.agentProgress,
158
353
  error,
159
354
  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."),
160
355
  promptArtifact,
@@ -162,6 +357,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
162
357
  claim: undefined,
163
358
  heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
164
359
  ...(logArtifact ? { logArtifact } : {}),
360
+ ...(transcriptArtifact ? { transcriptArtifact } : {}),
165
361
  };
166
362
  tasks = updateTask(tasks, task);
167
363
  const packetArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -176,9 +372,23 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
176
372
  content: `${JSON.stringify(task.verification, null, 2)}\n`,
177
373
  producer: task.id,
178
374
  });
179
- manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, packetArtifact, verificationArtifact, ...(logArtifact ? [logArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
375
+ const sharedOutputArtifact = writeTaskSharedOutput(manifest, input.step, task);
376
+ const startupArtifact = writeArtifact(manifest.artifactsRoot, {
377
+ kind: "metadata",
378
+ relativePath: `metadata/${task.id}.startup-evidence.json`,
379
+ content: `${JSON.stringify(startupEvidence, null, 2)}\n`,
380
+ producer: task.id,
381
+ });
382
+ const permissionArtifact = writeArtifact(manifest.artifactsRoot, {
383
+ kind: "metadata",
384
+ relativePath: `metadata/${task.id}.permission.json`,
385
+ content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
386
+ producer: task.id,
387
+ });
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] : [])] };
180
389
  saveRunManifest(manifest);
181
390
  saveRunTasks(manifest, tasks);
391
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, input.executeWorkers ? "child-process" : "scaffold"));
182
392
  appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
183
393
  return { manifest, tasks };
184
394
  }