pi-crew 0.1.5 → 0.1.7

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.
Files changed (35) hide show
  1. package/package.json +2 -1
  2. package/schema.json +24 -0
  3. package/src/agents/agent-config.ts +2 -0
  4. package/src/agents/discover-agents.ts +29 -5
  5. package/src/config/config.ts +148 -9
  6. package/src/extension/register.ts +10 -2
  7. package/src/extension/team-tool.ts +113 -10
  8. package/src/prompt/prompt-runtime.ts +12 -2
  9. package/src/runtime/agent-control.ts +64 -0
  10. package/src/runtime/agent-observability.ts +88 -0
  11. package/src/runtime/async-runner.ts +30 -1
  12. package/src/runtime/background-runner.ts +4 -2
  13. package/src/runtime/child-pi.ts +137 -7
  14. package/src/runtime/crew-agent-records.ts +137 -0
  15. package/src/runtime/crew-agent-runtime.ts +54 -0
  16. package/src/runtime/foreground-control.ts +82 -0
  17. package/src/runtime/group-join.ts +88 -0
  18. package/src/runtime/live-session-runtime.ts +33 -0
  19. package/src/runtime/pi-args.ts +29 -0
  20. package/src/runtime/policy-engine.ts +23 -0
  21. package/src/runtime/recovery-recipes.ts +74 -0
  22. package/src/runtime/role-permission.ts +28 -0
  23. package/src/runtime/runtime-resolver.ts +75 -0
  24. package/src/runtime/session-usage.ts +79 -0
  25. package/src/runtime/task-graph-scheduler.ts +107 -0
  26. package/src/runtime/task-output-context.ts +106 -0
  27. package/src/runtime/task-runner.ts +220 -4
  28. package/src/runtime/team-runner.ts +86 -14
  29. package/src/runtime/worker-startup.ts +57 -0
  30. package/src/state/contracts.ts +7 -0
  31. package/src/state/event-log.ts +103 -2
  32. package/src/state/state-store.ts +23 -2
  33. package/src/state/types.ts +3 -0
  34. package/src/ui/run-dashboard.ts +82 -10
  35. package/src/worktree/branch-freshness.ts +45 -0
@@ -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,7 @@
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 { CrewLimitsConfig } from "../config/config.ts";
4
+ import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
3
5
  import { writeArtifact } from "../state/artifact-store.ts";
4
6
  import { appendEvent } from "../state/event-log.ts";
5
7
  import { saveRunManifest, saveRunTasks } from "../state/state-store.ts";
@@ -12,6 +14,12 @@ import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts"
12
14
  import { runChildPi } from "./child-pi.ts";
13
15
  import { buildTaskPacket, renderTaskPacket } from "./task-packet.ts";
14
16
  import { createVerificationEvidence } from "./green-contract.ts";
17
+ import { createStartupEvidence } from "./worker-startup.ts";
18
+ import { permissionForRole } from "./role-permission.ts";
19
+ import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
20
+ import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
21
+ import { parseSessionUsage } from "./session-usage.ts";
22
+ import type { CrewAgentProgress } from "./crew-agent-runtime.ts";
15
23
 
16
24
  export interface TaskRunnerInput {
17
25
  manifest: TeamRunManifest;
@@ -21,6 +29,32 @@ export interface TaskRunnerInput {
21
29
  agent: AgentConfig;
22
30
  signal?: AbortSignal;
23
31
  executeWorkers: boolean;
32
+ limits?: CrewLimitsConfig;
33
+ dependencyContextText?: string;
34
+ }
35
+
36
+ function readOnlyRoleInstructions(role: string): string {
37
+ if (permissionForRole(role) !== "read_only") return "";
38
+ return [
39
+ "# READ-ONLY ROLE CONTRACT",
40
+ "You are running in READ-ONLY mode for this task.",
41
+ "- Do not create, modify, delete, move, or copy files.",
42
+ "- Do not use shell redirects, heredocs, in-place edits, package installs, git commit/merge/rebase/reset/checkout, or other state-mutating commands.",
43
+ "- If implementation changes are needed, report exact recommendations instead of applying them.",
44
+ "- Prefer read/grep/find/listing tools and read-only git inspection commands.",
45
+ ].join("\n");
46
+ }
47
+
48
+ function coordinationBridgeInstructions(task: TeamTaskState): string {
49
+ return [
50
+ "# Crew Coordination Channel",
51
+ `Mailbox target for this task: ${task.id}`,
52
+ "Use the run mailbox contract for coordination with the leader/orchestrator:",
53
+ "- 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.",
54
+ "- If nudged, answer with current status, blocker, or smallest next step.",
55
+ "- Treat inherited/dependency context as reference-only; do not continue the parent conversation directly.",
56
+ "- Completion handoff should include: DONE/FAILED, summary, changed/read files, verification evidence, and remaining risks.",
57
+ ].join("\n");
24
58
  }
25
59
 
26
60
  function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): string {
@@ -47,21 +81,139 @@ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: T
47
81
  "- Do not claim completion without evidence.",
48
82
  "- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
49
83
  "",
84
+ readOnlyRoleInstructions(task.role),
85
+ "",
86
+ coordinationBridgeInstructions(task),
87
+ "",
50
88
  task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
89
+ "",
90
+ (inputDependencyContext(task) || ""),
51
91
  "Task:",
52
92
  step.task.replaceAll("{goal}", manifest.goal),
53
93
  ].join("\n");
54
94
  }
55
95
 
96
+ function inputDependencyContext(task: TeamTaskState): string {
97
+ return (task as TeamTaskState & { dependencyContextText?: string }).dependencyContextText ?? "";
98
+ }
99
+
56
100
  function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
57
101
  return tasks.map((task) => task.id === updated.id ? updated : task);
58
102
  }
59
103
 
104
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
105
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
106
+ }
107
+
108
+ function textFromContent(content: unknown): string[] {
109
+ if (typeof content === "string") return [content];
110
+ if (!Array.isArray(content)) return [];
111
+ const text: string[] = [];
112
+ for (const part of content) {
113
+ const obj = asRecord(part);
114
+ if (!obj) continue;
115
+ if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
116
+ else if (typeof obj.content === "string") text.push(obj.content);
117
+ }
118
+ return text;
119
+ }
120
+
121
+ function eventText(event: unknown): string[] {
122
+ const obj = asRecord(event);
123
+ if (!obj) return [];
124
+ const text: string[] = [];
125
+ if (typeof obj.text === "string") text.push(obj.text);
126
+ if (typeof obj.output === "string") text.push(obj.output);
127
+ text.push(...textFromContent(obj.content));
128
+ const message = asRecord(obj.message);
129
+ if (message) text.push(...textFromContent(message.content));
130
+ return text.filter((entry) => entry.trim());
131
+ }
132
+
133
+ function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
134
+ for (const key of keys) {
135
+ const value = obj[key];
136
+ if (typeof value === "number" && Number.isFinite(value)) return value;
137
+ }
138
+ return undefined;
139
+ }
140
+
141
+ function eventUsage(event: unknown): { input?: number; output?: number; turns?: number } | undefined {
142
+ const obj = asRecord(event);
143
+ if (!obj) return undefined;
144
+ const direct = {
145
+ input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
146
+ output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
147
+ turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
148
+ };
149
+ if (Object.values(direct).some((value) => value !== undefined)) return direct;
150
+ for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
151
+ const nested = eventUsage(obj[key]);
152
+ if (nested) return nested;
153
+ }
154
+ const message = asRecord(obj.message);
155
+ return message ? eventUsage(message.usage) : undefined;
156
+ }
157
+
158
+ function previewArgs(args: unknown): string | undefined {
159
+ if (!args) return undefined;
160
+ try {
161
+ const text = typeof args === "string" ? args : JSON.stringify(args);
162
+ return text.length > 240 ? `${text.slice(0, 240)}…` : text;
163
+ } catch {
164
+ return undefined;
165
+ }
166
+ }
167
+
168
+ function applyUsageToProgress(progress: CrewAgentProgress | undefined, usage: UsageState | undefined): CrewAgentProgress | undefined {
169
+ if (!usage) return progress;
170
+ const base = progress ?? emptyCrewAgentProgress();
171
+ return {
172
+ ...base,
173
+ tokens: (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0),
174
+ turns: usage.turns ?? base.turns,
175
+ };
176
+ }
177
+
178
+ function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress {
179
+ const obj = asRecord(event);
180
+ const now = new Date().toISOString();
181
+ const next: CrewAgentProgress = { ...progress, recentTools: [...progress.recentTools], recentOutput: [...progress.recentOutput], lastActivityAt: now, activityState: "active" };
182
+ if (startedAt) next.durationMs = Date.now() - new Date(startedAt).getTime();
183
+ if (obj?.type === "tool_execution_start") {
184
+ next.toolCount += 1;
185
+ next.currentTool = typeof obj.toolName === "string" ? obj.toolName : typeof obj.name === "string" ? obj.name : "tool";
186
+ next.currentToolArgs = previewArgs(obj.args);
187
+ next.currentToolStartedAt = now;
188
+ }
189
+ if (obj?.type === "tool_execution_end") {
190
+ if (next.currentTool) next.recentTools.push({ tool: next.currentTool, args: next.currentToolArgs, endedAt: now });
191
+ next.currentTool = undefined;
192
+ next.currentToolArgs = undefined;
193
+ next.currentToolStartedAt = undefined;
194
+ }
195
+ if ((obj?.type === "tool_execution_error" || obj?.type === "tool_execution_failed") && next.currentTool) {
196
+ next.failedTool = next.currentTool;
197
+ }
198
+ const usage = eventUsage(event);
199
+ if (usage) {
200
+ next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
201
+ next.turns = usage.turns ?? next.turns;
202
+ }
203
+ const text = eventText(event);
204
+ if (text.length > 0) next.recentOutput.push(...text.flatMap((entry) => entry.split(/\r?\n/)).filter(Boolean).slice(-10));
205
+ if (next.recentTools.length > 25) next.recentTools.splice(0, next.recentTools.length - 25);
206
+ if (next.recentOutput.length > 50) next.recentOutput.splice(0, next.recentOutput.length - 50);
207
+ return next;
208
+ }
209
+
60
210
  export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
61
211
  let manifest = input.manifest;
62
212
  const workspace = prepareTaskWorkspace(manifest, input.task);
63
213
  const worktree = workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree;
64
214
  const taskPacket = buildTaskPacket({ manifest, step: input.step, taskId: input.task.id, cwd: workspace.cwd, worktreePath: worktree?.path });
215
+ const dependencyContext = collectDependencyOutputContext(manifest, input.tasks, input.task, input.step);
216
+ const dependencyContextText = input.dependencyContextText ?? renderDependencyOutputContext(dependencyContext);
65
217
  let task: TeamTaskState = {
66
218
  ...input.task,
67
219
  cwd: workspace.cwd,
@@ -71,10 +223,14 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
71
223
  startedAt: new Date().toISOString(),
72
224
  claim: createTaskClaim(`task-runner:${input.task.id}`),
73
225
  heartbeat: createWorkerHeartbeat(input.task.id),
74
- };
226
+ agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
227
+ ...(dependencyContextText ? { dependencyContextText } : {}),
228
+ } as TeamTaskState;
75
229
  let tasks = updateTask(input.tasks, task);
76
230
  saveRunTasks(manifest, tasks);
231
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, input.executeWorkers ? "child-process" : "scaffold"));
77
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 } });
233
+ const permissionMode = permissionForRole(task.role);
78
234
 
79
235
  const prompt = renderTaskPrompt(manifest, input.step, task);
80
236
  const promptArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -86,11 +242,20 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
86
242
 
87
243
  let resultArtifact: ArtifactDescriptor;
88
244
  let logArtifact: ArtifactDescriptor | undefined;
245
+ let transcriptArtifact: ArtifactDescriptor | undefined;
89
246
  let exitCode: number | null = 0;
90
247
  let error: string | undefined;
91
248
  let modelAttempts: ModelAttemptSummary[] | undefined;
92
249
  let parsedOutput: ParsedPiJsonOutput | undefined;
93
250
 
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 });
252
+ const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
253
+ const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
254
+ kind: "metadata",
255
+ relativePath: `metadata/${task.id}.coordination-bridge.md`,
256
+ content: `${coordinationBridgeInstructions(task)}\n`,
257
+ producer: task.id,
258
+ });
94
259
  if (input.executeWorkers) {
95
260
  const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
96
261
  const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
@@ -98,9 +263,28 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
98
263
  let finalStdout = "";
99
264
  let finalStderr = "";
100
265
  modelAttempts = [];
266
+ const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
101
267
  for (let i = 0; i < attemptModels.length; i++) {
102
268
  const model = attemptModels[i];
103
- const childResult = await runChildPi({ cwd: task.cwd, task: prompt, agent: input.agent, model, signal: input.signal });
269
+ const attemptStartedAt = new Date();
270
+ const childResult = await runChildPi({
271
+ cwd: task.cwd,
272
+ task: prompt,
273
+ agent: input.agent,
274
+ model,
275
+ signal: input.signal,
276
+ transcriptPath,
277
+ maxDepth: input.limits?.maxTaskDepth,
278
+ onStdoutLine: (line) => appendCrewAgentOutput(manifest, task.id, line),
279
+ onJsonEvent: (event) => {
280
+ appendCrewAgentEvent(manifest, task.id, event);
281
+ task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
282
+ tasks = updateTask(tasks, task);
283
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
284
+ appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { event } });
285
+ },
286
+ });
287
+ 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
288
  exitCode = childResult.exitCode;
105
289
  finalStdout = childResult.stdout;
106
290
  finalStderr = childResult.stderr;
@@ -126,6 +310,22 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
126
310
  content: [...logs, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`, parsedOutput?.usage ? `usage=${JSON.stringify(parsedOutput.usage)}` : "", "", "STDOUT:", finalStdout, "", "STDERR:", finalStderr].join("\n"),
127
311
  producer: task.id,
128
312
  });
313
+ const sessionUsage = parseSessionUsage(transcriptPath);
314
+ const effectiveUsage = parsedOutput?.usage ?? sessionUsage;
315
+ if (effectiveUsage) {
316
+ parsedOutput = { ...(parsedOutput ?? { jsonEvents: 0, textEvents: [] }), usage: effectiveUsage };
317
+ task = { ...task, usage: effectiveUsage, agentProgress: applyUsageToProgress(task.agentProgress, effectiveUsage) };
318
+ tasks = updateTask(tasks, task);
319
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
320
+ }
321
+ if (fs.existsSync(transcriptPath)) {
322
+ transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
323
+ kind: "log",
324
+ relativePath: `transcripts/${task.id}.jsonl`,
325
+ content: fs.readFileSync(transcriptPath, "utf-8"),
326
+ producer: task.id,
327
+ });
328
+ }
129
329
  } else {
130
330
  resultArtifact = writeArtifact(manifest.artifactsRoot, {
131
331
  kind: "result",
@@ -155,6 +355,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
155
355
  modelAttempts,
156
356
  usage: parsedOutput?.usage,
157
357
  jsonEvents: parsedOutput?.jsonEvents,
358
+ agentProgress: error && task.agentProgress?.currentTool ? { ...task.agentProgress, failedTool: task.agentProgress.currentTool } : task.agentProgress,
158
359
  error,
159
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."),
160
361
  promptArtifact,
@@ -162,6 +363,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
162
363
  claim: undefined,
163
364
  heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
164
365
  ...(logArtifact ? { logArtifact } : {}),
366
+ ...(transcriptArtifact ? { transcriptArtifact } : {}),
165
367
  };
166
368
  tasks = updateTask(tasks, task);
167
369
  const packetArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -176,9 +378,23 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
176
378
  content: `${JSON.stringify(task.verification, null, 2)}\n`,
177
379
  producer: task.id,
178
380
  });
179
- manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, packetArtifact, verificationArtifact, ...(logArtifact ? [logArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
381
+ const sharedOutputArtifact = writeTaskSharedOutput(manifest, input.step, task);
382
+ const startupArtifact = writeArtifact(manifest.artifactsRoot, {
383
+ kind: "metadata",
384
+ relativePath: `metadata/${task.id}.startup-evidence.json`,
385
+ content: `${JSON.stringify(startupEvidence, null, 2)}\n`,
386
+ producer: task.id,
387
+ });
388
+ const permissionArtifact = writeArtifact(manifest.artifactsRoot, {
389
+ kind: "metadata",
390
+ relativePath: `metadata/${task.id}.permission.json`,
391
+ content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
392
+ producer: task.id,
393
+ });
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] : [])] };
180
395
  saveRunManifest(manifest);
181
396
  saveRunTasks(manifest, tasks);
397
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, input.executeWorkers ? "child-process" : "scaffold"));
182
398
  appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
183
399
  return { manifest, tasks };
184
400
  }