pi-crew 0.2.19 → 0.2.21

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 (93) hide show
  1. package/CHANGELOG.md +23 -10
  2. package/README.md +4 -2
  3. package/docs/PROJECT_REVIEW.md +271 -0
  4. package/docs/PROJECT_REVIEW_FIXES.md +343 -0
  5. package/docs/PROJECT_REVIEW_ROUND4.md +156 -0
  6. package/docs/PROJECT_REVIEW_ROUND5.md +86 -0
  7. package/docs/fixes/BATCH_A_H1_H2.md +86 -0
  8. package/docs/fixes/bug-006-foreground-cancel-concurrent.md +78 -0
  9. package/docs/fixes/bug-007-async-notifier-stale-ctx.md +112 -0
  10. package/docs/fixes/bug-008-child-process-silent-timeout.md +100 -0
  11. package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +75 -0
  12. package/docs/fixes/bug-010-child-process-api-key-filtered.md +109 -0
  13. package/docs/fixes/bug-011-spawn-pi-enoent.md +92 -0
  14. package/docs/fixes/bug-012-essential-env-stripped.md +89 -0
  15. package/docs/fixes/bug-013-background-runner-death.md +84 -0
  16. package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +82 -0
  17. package/docs/fixes/bug-015-background-runner-sigterm.md +65 -0
  18. package/docs/fixes/bug-017-background-runner-session-shutdown.md +66 -0
  19. package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +28 -0
  20. package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +61 -0
  21. package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +52 -0
  22. package/docs/pi-crew-bugs.md +954 -0
  23. package/docs/pi-crew-investigation-report.md +411 -0
  24. package/docs/pi-crew-test-final.md +120 -0
  25. package/docs/pi-crew-test-results.md +260 -0
  26. package/docs/pi-crew-test-round2.md +136 -0
  27. package/docs/pi-crew-test-round4.md +100 -0
  28. package/docs/pi-crew-test-round5.md +70 -0
  29. package/docs/pi-crew-test-round6.md +110 -0
  30. package/docs/usage.md +14 -0
  31. package/package.json +7 -5
  32. package/src/adapters/export-util.ts +12 -6
  33. package/src/agents/agent-config.ts +2 -0
  34. package/src/config/defaults.ts +1 -1
  35. package/src/config/markers.ts +22 -17
  36. package/src/config/resilient-parser.ts +1 -1
  37. package/src/extension/async-notifier.ts +4 -2
  38. package/src/extension/management.ts +52 -0
  39. package/src/extension/register.ts +47 -10
  40. package/src/extension/run-index.ts +20 -2
  41. package/src/extension/run-maintenance.ts +2 -2
  42. package/src/extension/team-tool/parallel-dispatch.ts +1 -1
  43. package/src/extension/team-tool/run.ts +3 -6
  44. package/src/extension/team-tool.ts +67 -11
  45. package/src/observability/event-to-metric.ts +2 -1
  46. package/src/runtime/async-runner.ts +42 -34
  47. package/src/runtime/background-runner.ts +165 -7
  48. package/src/runtime/child-pi.ts +111 -18
  49. package/src/runtime/code-summary.ts +1 -1
  50. package/src/runtime/crash-recovery.ts +1 -1
  51. package/src/runtime/crew-agent-runtime.ts +2 -1
  52. package/src/runtime/heartbeat-watcher.ts +4 -0
  53. package/src/runtime/live-agent-manager.ts +1 -1
  54. package/src/runtime/live-session-runtime.ts +2 -1
  55. package/src/runtime/manifest-cache.ts +2 -2
  56. package/src/runtime/model-fallback.ts +2 -1
  57. package/src/runtime/phase-progress.ts +1 -1
  58. package/src/runtime/pi-args.ts +3 -1
  59. package/src/runtime/pi-spawn.ts +6 -0
  60. package/src/runtime/prose-compressor.ts +1 -1
  61. package/src/runtime/result-extractor.ts +0 -1
  62. package/src/runtime/retry-executor.ts +1 -1
  63. package/src/runtime/runtime-resolver.ts +1 -1
  64. package/src/runtime/skill-instructions.ts +0 -1
  65. package/src/runtime/stale-reconciler.ts +30 -3
  66. package/src/runtime/subagent-manager.ts +2 -0
  67. package/src/runtime/task-display.ts +1 -1
  68. package/src/runtime/task-graph-scheduler.ts +1 -1
  69. package/src/runtime/task-runner/tail-read.ts +26 -0
  70. package/src/runtime/task-runner.ts +1007 -383
  71. package/src/runtime/team-runner.ts +9 -5
  72. package/src/runtime/worker-startup.ts +3 -1
  73. package/src/schema/team-tool-schema.ts +2 -1
  74. package/src/state/active-run-registry.ts +8 -2
  75. package/src/state/atomic-write.ts +17 -0
  76. package/src/state/contracts.ts +5 -2
  77. package/src/state/event-log-rotation.ts +118 -31
  78. package/src/state/event-log.ts +33 -5
  79. package/src/state/event-reconstructor.ts +4 -2
  80. package/src/state/mailbox.ts +5 -1
  81. package/src/state/schedule.ts +146 -0
  82. package/src/state/types.ts +40 -0
  83. package/src/state/usage.ts +20 -0
  84. package/src/ui/crew-widget.ts +2 -2
  85. package/src/ui/run-event-bus.ts +1 -1
  86. package/src/ui/run-snapshot-cache.ts +2 -1
  87. package/src/ui/snapshot-types.ts +1 -0
  88. package/src/utils/gh-protocol.ts +2 -2
  89. package/src/utils/names.ts +1 -1
  90. package/src/utils/sse-parser.ts +0 -2
  91. package/src/worktree/branch-freshness.ts +1 -1
  92. package/src/worktree/cleanup.ts +54 -14
  93. package/src/worktree/worktree-manager.ts +19 -9
@@ -1,42 +1,116 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { AgentConfig } from "../agents/agent-config.ts";
3
3
  import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
4
- import type { ArtifactDescriptor, OperationTerminalEvidence, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
4
+ import type {
5
+ ArtifactDescriptor,
6
+ OperationTerminalEvidence,
7
+ TeamRunManifest,
8
+ TeamTaskState,
9
+ UsageState,
10
+ } from "../state/types.ts";
11
+ import { logInternalError } from "../utils/internal-error.ts";
5
12
  import { writeArtifact } from "../state/artifact-store.ts";
6
13
  import { appendEvent, appendEventFireAndForget } from "../state/event-log.ts";
7
14
  import { saveRunManifest } from "../state/state-store.ts";
8
15
  import { createTaskClaim } from "../state/task-claims.ts";
9
- import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
16
+ import {
17
+ createWorkerHeartbeat,
18
+ touchWorkerHeartbeat,
19
+ } from "./worker-heartbeat.ts";
10
20
  import type { WorkflowStep } from "../workflows/workflow-config.ts";
11
- import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
12
- import { buildConfiguredModelRouting, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
13
- import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
21
+ import {
22
+ captureWorktreeDiff,
23
+ captureWorktreeDiffStat,
24
+ prepareTaskWorkspace,
25
+ } from "../worktree/worktree-manager.ts";
26
+ import {
27
+ buildConfiguredModelRouting,
28
+ formatModelAttemptNote,
29
+ isRetryableModelFailure,
30
+ type ModelAttemptSummary,
31
+ } from "./model-fallback.ts";
32
+ import { tailReadWithLineSnap } from "./task-runner/tail-read.ts";
33
+ import {
34
+ parsePiJsonOutput,
35
+ type ParsedPiJsonOutput,
36
+ } from "./pi-json-output.ts";
14
37
  import { runChildPi, type ChildPiLifecycleEvent } from "./child-pi.ts";
15
38
  import { buildTaskPacket } from "./task-packet.ts";
16
39
  import { executeHook, appendHookEvent } from "../hooks/registry.ts";
17
40
  import { createVerificationEvidence } from "./green-contract.ts";
18
41
  import { createStartupEvidence } from "./worker-startup.ts";
19
42
  import { permissionForRole } from "./role-permission.ts";
20
- import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
21
- import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
43
+ import {
44
+ collectDependencyOutputContext,
45
+ renderDependencyOutputContext,
46
+ writeTaskInputsArtifact,
47
+ writeTaskSharedOutput,
48
+ } from "./task-output-context.ts";
49
+ import {
50
+ appendCrewAgentEvent,
51
+ appendCrewAgentOutput,
52
+ emptyCrewAgentProgress,
53
+ recordFromTask,
54
+ upsertCrewAgent,
55
+ } from "./crew-agent-records.ts";
22
56
  import { reserveControlChannel } from "./agent-control.ts";
23
57
  import { parseSessionUsage } from "./session-usage.ts";
24
- import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
25
- import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
26
- import { coordinationBridgeInstructions, renderTaskPrompt } from "./task-runner/prompt-builder.ts";
58
+ import type {
59
+ CrewAgentProgress,
60
+ CrewRuntimeKind,
61
+ } from "./crew-agent-runtime.ts";
62
+ import {
63
+ shouldAppendProgressEventUpdate,
64
+ type ProgressEventSummary,
65
+ } from "./progress-event-coalescer.ts";
66
+ import {
67
+ coordinationBridgeInstructions,
68
+ renderTaskPrompt,
69
+ } from "./task-runner/prompt-builder.ts";
27
70
  import { buildWorkerPromptPipeline } from "./task-runner/prompt-pipeline.ts";
28
71
  import { buildWorkerCapabilityInventory } from "./task-runner/capabilities.ts";
29
- import { applyAgentProgressEvent, applyUsageToProgress, progressEventSummary, shouldFlushProgressEvent } from "./task-runner/progress.ts";
30
- import { checkpointTask, persistSingleTaskUpdate, updateTask } from "./task-runner/state-helpers.ts";
31
- import { cleanResultText, isFinalChildEvent } from "./task-runner/result-utils.ts";
72
+ import {
73
+ applyAgentProgressEvent,
74
+ applyUsageToProgress,
75
+ progressEventSummary,
76
+ shouldFlushProgressEvent,
77
+ } from "./task-runner/progress.ts";
78
+ import {
79
+ checkpointTask,
80
+ persistSingleTaskUpdate,
81
+ updateTask,
82
+ } from "./task-runner/state-helpers.ts";
83
+ import {
84
+ cleanResultText,
85
+ isFinalChildEvent,
86
+ } from "./task-runner/result-utils.ts";
32
87
  import { evaluateCompletionMutationGuard } from "./completion-guard.ts";
33
- import { cancellationReasonFromSignal, buildSyntheticTerminalEvidence } from "./cancellation.ts";
88
+ import {
89
+ cancellationReasonFromSignal,
90
+ buildSyntheticTerminalEvidence,
91
+ } from "./cancellation.ts";
34
92
  import { appendTaskAttentionEvent } from "./attention-events.ts";
35
- import { parseSupervisorContactFromLine, recordSupervisorContact } from "./supervisor-contact.ts";
36
- import { registerStreamBridge, bridgeEventFromJsonEvent } from "./event-stream-bridge.ts";
93
+ import {
94
+ parseSupervisorContactFromLine,
95
+ recordSupervisorContact,
96
+ } from "./supervisor-contact.ts";
97
+ import {
98
+ registerStreamBridge,
99
+ bridgeEventFromJsonEvent,
100
+ } from "./event-stream-bridge.ts";
37
101
  import { renderSkillInstructions } from "./skill-instructions.ts";
38
- import { DEFAULT_YIELD_CONFIG, extractYieldResult, hasYieldInOutput, isYieldEvent, registerYieldTool, type YieldResult } from "./yield-handler.ts";
39
- import { validateWorkerOutput, type OutputValidationResult } from "./output-validator.ts";
102
+ import {
103
+ DEFAULT_YIELD_CONFIG,
104
+ extractYieldResult,
105
+ hasYieldInOutput,
106
+ isYieldEvent,
107
+ registerYieldTool,
108
+ type YieldResult,
109
+ } from "./yield-handler.ts";
110
+ import {
111
+ validateWorkerOutput,
112
+ type OutputValidationResult,
113
+ } from "./output-validator.ts";
40
114
 
41
115
  // Register the submit_result tool handler so subprocess events can extract yield data.
42
116
  registerYieldTool();
@@ -71,398 +145,948 @@ export interface TaskRunnerInput {
71
145
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
72
146
  }
73
147
 
74
- export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
148
+ export async function runTeamTask(
149
+ input: TaskRunnerInput,
150
+ ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
75
151
  let manifest = input.manifest;
76
152
  // H4: registerStreamBridge inside try so dispose() in finally is safe
77
153
  let streamBridge: ReturnType<typeof registerStreamBridge> | undefined;
78
154
  try {
79
- streamBridge = registerStreamBridge(manifest.runId);
80
- const workspace = prepareTaskWorkspace(manifest, input.task);
81
- const worktree = workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree;
82
- const taskPacket = buildTaskPacket({ manifest, step: input.step, taskId: input.task.id, cwd: workspace.cwd, worktreePath: worktree?.path });
83
- const dependencyContext = collectDependencyOutputContext(manifest, input.tasks, input.task, input.step);
84
- const dependencyContextText = input.dependencyContextText ?? renderDependencyOutputContext(dependencyContext);
85
- let task: TeamTaskState = {
86
- ...input.task,
87
- cwd: workspace.cwd,
88
- worktree,
89
- taskPacket,
90
- status: "running",
91
- startedAt: new Date().toISOString(),
92
- claim: createTaskClaim(`task-runner:${input.task.id}`),
93
- heartbeat: createWorkerHeartbeat(input.task.id),
94
- agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
95
- ...(dependencyContextText ? { dependencyContextText } : {}),
96
- // Reserve control channel before spawn so cancel/steer can target this task immediately
97
- controlReservation: reserveControlChannel(input.task.id, manifest.runId),
98
- } as TeamTaskState;
99
- let tasks = updateTask(input.tasks, task);
100
- const runtimeKind = input.taskRuntimeOverride ?? input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
101
- tasks = persistSingleTaskUpdate(manifest, tasks, task);
102
- if (runtimeKind === "child-process") ({ task, tasks } = checkpointTask(manifest, tasks, task, "started"));
103
- upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
104
- 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 } });
105
- // Emit immediate UI notification so widget shows agent as "running" within ~100ms
106
- // instead of waiting for child process first JSON event (2-5s delay).
107
- streamBridge?.handler({ runId: manifest.runId, taskId: task.id, eventType: "task.started", timestamp: Date.now() });
108
- const permissionMode = permissionForRole(task.role);
109
- const renderedSkills = input.skillBlock === undefined ? renderSkillInstructions({ cwd: task.cwd, role: task.role, agent: input.agent, teamRole: { skills: input.teamRoleSkills }, step: input.step, override: input.skillOverride }) : undefined;
110
- const skillBlock = input.skillBlock ?? renderedSkills?.block;
111
- const skillNames = input.skillNames ?? renderedSkills?.names;
112
- const skillPaths = input.skillPaths ?? renderedSkills?.paths;
155
+ streamBridge = registerStreamBridge(manifest.runId);
156
+ const workspace = prepareTaskWorkspace(manifest, input.task);
157
+ const worktree =
158
+ workspace.worktreePath && workspace.branch
159
+ ? {
160
+ path: workspace.worktreePath,
161
+ branch: workspace.branch,
162
+ reused: workspace.reused ?? false,
163
+ }
164
+ : input.task.worktree;
165
+ const taskPacket = buildTaskPacket({
166
+ manifest,
167
+ step: input.step,
168
+ taskId: input.task.id,
169
+ cwd: workspace.cwd,
170
+ worktreePath: worktree?.path,
171
+ });
172
+ const dependencyContext = collectDependencyOutputContext(
173
+ manifest,
174
+ input.tasks,
175
+ input.task,
176
+ input.step,
177
+ );
178
+ const dependencyContextText =
179
+ input.dependencyContextText ??
180
+ renderDependencyOutputContext(dependencyContext);
181
+ let task: TeamTaskState = {
182
+ ...input.task,
183
+ cwd: workspace.cwd,
184
+ worktree,
185
+ taskPacket,
186
+ status: "running",
187
+ startedAt: new Date().toISOString(),
188
+ claim: createTaskClaim(`task-runner:${input.task.id}`),
189
+ heartbeat: createWorkerHeartbeat(input.task.id),
190
+ agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
191
+ // Lifetime usage accumulator — survives compaction unlike session.stats
192
+ lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
193
+ ...(dependencyContextText ? { dependencyContextText } : {}),
194
+ // Reserve control channel before spawn so cancel/steer can target this task immediately
195
+ controlReservation: reserveControlChannel(
196
+ input.task.id,
197
+ manifest.runId,
198
+ ),
199
+ } as TeamTaskState;
200
+ let tasks = updateTask(input.tasks, task);
201
+ const runtimeKind =
202
+ input.taskRuntimeOverride ??
203
+ input.runtimeKind ??
204
+ (input.executeWorkers ? "child-process" : "scaffold");
205
+ tasks = persistSingleTaskUpdate(manifest, tasks, task);
206
+ if (runtimeKind === "child-process")
207
+ ({ task, tasks } = checkpointTask(
208
+ manifest,
209
+ tasks,
210
+ task,
211
+ "started",
212
+ ));
213
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
214
+ appendEvent(manifest.eventsPath, {
215
+ type: "task.started",
216
+ runId: manifest.runId,
217
+ taskId: task.id,
218
+ data: {
219
+ role: task.role,
220
+ agent: task.agent,
221
+ runtime: runtimeKind,
222
+ cwd: task.cwd,
223
+ worktreePath: workspace.worktreePath,
224
+ worktreeBranch: workspace.branch,
225
+ worktreeReused: workspace.reused,
226
+ },
227
+ });
228
+ // Emit immediate UI notification so widget shows agent as "running" within ~100ms
229
+ // instead of waiting for child process first JSON event (2-5s delay).
230
+ streamBridge?.handler({
231
+ runId: manifest.runId,
232
+ taskId: task.id,
233
+ eventType: "task.started",
234
+ timestamp: Date.now(),
235
+ });
236
+ const permissionMode = permissionForRole(task.role);
237
+ const renderedSkills =
238
+ input.skillBlock === undefined
239
+ ? renderSkillInstructions({
240
+ cwd: task.cwd,
241
+ role: task.role,
242
+ agent: input.agent,
243
+ teamRole: { skills: input.teamRoleSkills },
244
+ step: input.step,
245
+ override: input.skillOverride,
246
+ })
247
+ : undefined;
248
+ const skillBlock = input.skillBlock ?? renderedSkills?.block;
249
+ const skillNames = input.skillNames ?? renderedSkills?.names;
250
+ const skillPaths = input.skillPaths ?? renderedSkills?.paths;
113
251
 
114
- const promptResult = await renderTaskPrompt(manifest, input.step, task, input.agent, skillBlock);
115
- const prompt = promptResult.full;
116
- const promptArtifact = writeArtifact(manifest.artifactsRoot, {
117
- kind: "prompt",
118
- relativePath: `prompts/${task.id}.md`,
119
- content: `${prompt}\n`,
120
- producer: task.id,
121
- });
252
+ const promptResult = await renderTaskPrompt(
253
+ manifest,
254
+ input.step,
255
+ task,
256
+ input.agent,
257
+ skillBlock,
258
+ );
259
+ const prompt = promptResult.full;
260
+ const promptArtifact = writeArtifact(manifest.artifactsRoot, {
261
+ kind: "prompt",
262
+ relativePath: `prompts/${task.id}.md`,
263
+ content: `${prompt}\n`,
264
+ producer: task.id,
265
+ });
122
266
 
123
- let resultArtifact: ArtifactDescriptor;
124
- let logArtifact: ArtifactDescriptor | undefined;
125
- let transcriptArtifact: ArtifactDescriptor | undefined;
126
- let exitCode: number | null = 0;
127
- let error: string | undefined;
128
- let modelAttempts: ModelAttemptSummary[] | undefined;
129
- let parsedOutput: ParsedPiJsonOutput | undefined;
130
- let finalStdout = "";
131
- let transcriptPath: string | undefined;
132
- let terminalEvidence: OperationTerminalEvidence[] = [];
133
- const collectedJsonEvents: Record<string, unknown>[] = [];
267
+ let resultArtifact: ArtifactDescriptor;
268
+ let logArtifact: ArtifactDescriptor | undefined;
269
+ let transcriptArtifact: ArtifactDescriptor | undefined;
270
+ let exitCode: number | null = 0;
271
+ let error: string | undefined;
272
+ let modelAttempts: ModelAttemptSummary[] | undefined;
273
+ let parsedOutput: ParsedPiJsonOutput | undefined;
274
+ let finalStdout = "";
275
+ let transcriptPath: string | undefined;
276
+ let terminalEvidence: OperationTerminalEvidence[] = [];
277
+ const collectedJsonEvents: Record<string, unknown>[] = [];
134
278
 
135
- 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 });
136
- const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
137
- const skillArtifact = skillBlock ? writeArtifact(manifest.artifactsRoot, {
138
- kind: "metadata",
139
- relativePath: `metadata/${task.id}.skills.md`,
140
- content: [`Selected skills: ${skillNames?.join(", ") ?? "(none)"}`, `Skill paths passed to child Pi: ${(skillPaths ?? []).length}`, "", skillBlock, ""].join("\n"),
141
- producer: task.id,
142
- }) : undefined;
143
- const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
144
- kind: "metadata",
145
- relativePath: `metadata/${task.id}.coordination-bridge.md`,
146
- content: `${coordinationBridgeInstructions(task)}\n`,
147
- producer: task.id,
148
- });
149
- if (runtimeKind === "child-process") {
150
- const modelRoutingPlan = buildConfiguredModelRouting({ overrideModel: input.modelOverride, stepModel: input.step.model, teamRoleModel: input.teamRoleModel, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: task.cwd });
151
- const candidates = modelRoutingPlan.candidates;
152
- const attemptModels = candidates.length > 0 ? candidates : [undefined];
153
- const logs: string[] = [];
154
- let finalStderr = "";
155
- modelAttempts = [];
156
- transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
157
- let finalCheckpointWritten = false;
158
- let lastAgentRecordPersistedAt = 0;
159
- let lastHeartbeatPersistedAt = 0;
160
- let lastRunProgressPersistedAt = 0;
161
- let lastRunProgressSummary: ProgressEventSummary | undefined;
162
- const persistHeartbeat = (force = false): void => {
163
- const now = Date.now();
164
- if (!force && now - lastHeartbeatPersistedAt < 1000) return;
165
- lastHeartbeatPersistedAt = now;
166
- task = { ...task, heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id)) };
167
- tasks = persistSingleTaskUpdate(manifest, tasks, task);
168
- };
169
- const persistChildProgress = (event: unknown, force = false): void => {
170
- const now = Date.now();
171
- if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
172
- upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
173
- lastAgentRecordPersistedAt = now;
279
+ let startupEvidence = createStartupEvidence({
280
+ command:
281
+ runtimeKind === "child-process"
282
+ ? "pi"
283
+ : runtimeKind === "live-session"
284
+ ? "live-session"
285
+ : "safe-scaffold",
286
+ startedAt: new Date(task.startedAt ?? new Date().toISOString()),
287
+ finishedAt: new Date(),
288
+ promptSentAt: new Date(task.startedAt ?? new Date().toISOString()),
289
+ promptAccepted: true,
290
+ exitCode: 0,
291
+ });
292
+ const inputsArtifact = writeTaskInputsArtifact(
293
+ manifest,
294
+ task,
295
+ dependencyContext,
296
+ );
297
+ const skillArtifact = skillBlock
298
+ ? writeArtifact(manifest.artifactsRoot, {
299
+ kind: "metadata",
300
+ relativePath: `metadata/${task.id}.skills.md`,
301
+ content: [
302
+ `Selected skills: ${skillNames?.join(", ") ?? "(none)"}`,
303
+ `Skill paths passed to child Pi: ${(skillPaths ?? []).length}`,
304
+ "",
305
+ skillBlock,
306
+ "",
307
+ ].join("\n"),
308
+ producer: task.id,
309
+ })
310
+ : undefined;
311
+ const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
312
+ kind: "metadata",
313
+ relativePath: `metadata/${task.id}.coordination-bridge.md`,
314
+ content: `${coordinationBridgeInstructions(task)}\n`,
315
+ producer: task.id,
316
+ });
317
+ if (runtimeKind === "child-process") {
318
+ const modelRoutingPlan = buildConfiguredModelRouting({
319
+ overrideModel: input.modelOverride,
320
+ stepModel: input.step.model,
321
+ teamRoleModel: input.teamRoleModel,
322
+ agentModel: input.agent.model,
323
+ fallbackModels: input.agent.fallbackModels,
324
+ parentModel: input.parentModel,
325
+ modelRegistry: input.modelRegistry,
326
+ cwd: task.cwd,
327
+ });
328
+ const candidates = modelRoutingPlan.candidates;
329
+ const attemptModels =
330
+ candidates.length > 0 ? candidates : [undefined];
331
+ const logs: string[] = [];
332
+ let finalStderr = "";
333
+ modelAttempts = [];
334
+ let finalCheckpointWritten = false;
335
+ let lastAgentRecordPersistedAt = 0;
336
+ let lastHeartbeatPersistedAt = 0;
337
+ let lastRunProgressPersistedAt = 0;
338
+ let lastRunProgressSummary: ProgressEventSummary | undefined;
339
+ const persistHeartbeat = (force = false): void => {
340
+ const now = Date.now();
341
+ if (!force && now - lastHeartbeatPersistedAt < 1000) return;
342
+ lastHeartbeatPersistedAt = now;
343
+ task = {
344
+ ...task,
345
+ heartbeat: touchWorkerHeartbeat(
346
+ task.heartbeat ?? createWorkerHeartbeat(task.id),
347
+ ),
348
+ };
349
+ tasks = persistSingleTaskUpdate(manifest, tasks, task);
350
+ };
351
+ const persistChildProgress = (
352
+ event: unknown,
353
+ force = false,
354
+ ): void => {
355
+ const now = Date.now();
356
+ if (
357
+ force ||
358
+ shouldFlushProgressEvent(event) ||
359
+ now - lastAgentRecordPersistedAt >= 500
360
+ ) {
361
+ upsertCrewAgent(
362
+ manifest,
363
+ recordFromTask(manifest, task, "child-process"),
364
+ );
365
+ lastAgentRecordPersistedAt = now;
366
+ }
367
+ const summary = progressEventSummary(task, event);
368
+ const decision = shouldAppendProgressEventUpdate({
369
+ previous: lastRunProgressSummary,
370
+ next: summary,
371
+ nowMs: now,
372
+ lastAppendMs: lastRunProgressPersistedAt || undefined,
373
+ minIntervalMs: 1000,
374
+ force,
375
+ });
376
+ if (decision.shouldAppend) {
377
+ // 2.2 caller migration: high-frequency task.progress goes through
378
+ // the buffered path; loss-on-kill is acceptable because progress
379
+ // is informational and re-derivable from per-agent records.
380
+ appendEventFireAndForget(manifest.eventsPath, {
381
+ type: "task.progress",
382
+ runId: manifest.runId,
383
+ taskId: task.id,
384
+ data: { ...summary, coalesceReason: decision.reason },
385
+ });
386
+ lastRunProgressSummary = summary;
387
+ lastRunProgressPersistedAt = now;
388
+ }
389
+ };
390
+ for (let i = 0; i < attemptModels.length; i++) {
391
+ // M1 fix: set transcript path per attempt to avoid mixing across fallback attempts.
392
+ transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.attempt-${i}.jsonl`;
393
+ const model = attemptModels[i];
394
+ const attemptStartedAt = new Date();
395
+ const pendingAttempt: ModelAttemptSummary = {
396
+ model: model ?? "default",
397
+ success: false,
398
+ };
399
+ task = {
400
+ ...task,
401
+ modelAttempts: [...modelAttempts, pendingAttempt],
402
+ };
403
+ tasks = updateTask(tasks, task);
404
+ upsertCrewAgent(
405
+ manifest,
406
+ recordFromTask(manifest, task, "child-process"),
407
+ );
408
+ const childResult = await runChildPi({
409
+ cwd: task.cwd,
410
+ task: prompt,
411
+ agent: input.agent,
412
+ model,
413
+ signal: input.signal,
414
+ transcriptPath,
415
+ maxDepth: input.limits?.maxTaskDepth,
416
+ skillPaths,
417
+ maxTurns: input.runtimeConfig?.maxTurns,
418
+ graceTurns: input.runtimeConfig?.graceTurns,
419
+ onSpawn: (pid) => {
420
+ try {
421
+ ({ task, tasks } = checkpointTask(
422
+ manifest,
423
+ tasks,
424
+ task,
425
+ "child-spawned",
426
+ pid,
427
+ ));
428
+ if (task.pendingSteers?.length) {
429
+ const steeringDir = `${manifest.artifactsRoot}/steering`;
430
+ fs.mkdirSync(steeringDir, { recursive: true });
431
+ const steeringPath = `${steeringDir}/${task.id}.jsonl`;
432
+ for (const msg of task.pendingSteers) {
433
+ fs.appendFileSync(steeringPath, JSON.stringify({ type: "steer", message: msg, ts: new Date().toISOString() }) + "\n");
434
+ }
435
+ task.pendingSteers = [];
436
+ tasks = persistSingleTaskUpdate(manifest, tasks, task);
437
+ }
438
+ } catch (err) {
439
+ logInternalError("task-runner.on-spawn", err as Error, `pid=${pid}, taskId=${task.id}`);
440
+ }
441
+ },
442
+ onLifecycleEvent: (event: ChildPiLifecycleEvent) => {
443
+ appendEvent(manifest.eventsPath, {
444
+ type: `worker.${event.type}` as const,
445
+ runId: manifest.runId,
446
+ taskId: task.id,
447
+ message: `Worker lifecycle: ${event.type}${event.error ? ` error=${event.error}` : ""}${event.exitCode != null ? ` exit=${event.exitCode}` : ""}`,
448
+ data: { ...event },
449
+ });
450
+ },
451
+ onStdoutLine: (line) => {
452
+ appendCrewAgentOutput(manifest, task.id, line);
453
+ persistHeartbeat();
454
+ // Check for supervisor contact requests from child Pi
455
+ const contact = parseSupervisorContactFromLine(line);
456
+ if (contact) {
457
+ recordSupervisorContact(manifest, {
458
+ runId: manifest.runId,
459
+ ...contact,
460
+ });
461
+ }
462
+ },
463
+ onJsonEvent: (event) => {
464
+ // Top-level error boundary: prevent any single event from crashing the task.
465
+ // Errors are logged but processing continues so subsequent events still update state.
466
+ try {
467
+ appendCrewAgentEvent(manifest, task.id, event);
468
+ } catch (err) {
469
+ logInternalError("task-runner.append-crew-agent-event", err, `taskId=${task.id}`);
470
+ }
471
+ if (
472
+ event &&
473
+ typeof event === "object" &&
474
+ !Array.isArray(event)
475
+ )
476
+ collectedJsonEvents.push(
477
+ event as Record<string, unknown>,
478
+ );
479
+ // Accumulate lifetime usage via message_end events (survives compaction)
480
+ if (event && typeof event === "object" && (event as Record<string, unknown>).type === "message_end") {
481
+ const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
482
+ if (msg?.role === "assistant") {
483
+ const usage = msg.usage as Record<string, number> | undefined;
484
+ if (usage) {
485
+ task.lifetimeUsage = {
486
+ input: (task.lifetimeUsage?.input ?? 0) + (usage.input ?? 0),
487
+ output: (task.lifetimeUsage?.output ?? 0) + (usage.output ?? 0),
488
+ cacheWrite: (task.lifetimeUsage?.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
489
+ };
490
+ }
491
+ }
492
+ }
493
+ persistHeartbeat();
494
+ // Bug #3 fix: Write worker JSON events to background.log for debugging when running in background mode.
495
+ // This supplements the event log so developers can see what the child Pi worker produced.
496
+ if (process.env.PI_CREW_BACKGROUND_MODE === "1" && event) {
497
+ try {
498
+ const bgLogPath = `${manifest.stateRoot}/background.log`;
499
+ const eventLine = typeof event === "object" && !Array.isArray(event) ? JSON.stringify(event) : String(event);
500
+ fs.appendFileSync(bgLogPath, `${eventLine}\n`);
501
+ } catch { /* background log write failures should not affect task */ }
502
+ }
503
+ task = {
504
+ ...task,
505
+ agentProgress: applyAgentProgressEvent(
506
+ task.agentProgress ?? emptyCrewAgentProgress(),
507
+ event,
508
+ task.startedAt,
509
+ ),
510
+ };
511
+ tasks = updateTask(tasks, task);
512
+ // Bridge event to UI event bus for near-instant updates
513
+ try {
514
+ const bridgeEvent = bridgeEventFromJsonEvent(
515
+ manifest.runId,
516
+ task.id,
517
+ event,
518
+ );
519
+ if (bridgeEvent) streamBridge?.handler(bridgeEvent);
520
+ } catch {
521
+ /* bridge errors should not affect task */
522
+ }
523
+ // Feed overflow recovery tracker
524
+ if (input.onJsonEvent) {
525
+ try {
526
+ input.onJsonEvent(
527
+ task.id,
528
+ manifest.runId,
529
+ event,
530
+ );
531
+ } catch {
532
+ /* overflow tracking errors should not affect task */
533
+ }
534
+ }
535
+ if (
536
+ !finalCheckpointWritten &&
537
+ isFinalChildEvent(event)
538
+ ) {
539
+ finalCheckpointWritten = true;
540
+ ({ task, tasks } = checkpointTask(
541
+ manifest,
542
+ tasks,
543
+ task,
544
+ "child-stdout-final",
545
+ ));
546
+ }
547
+ persistChildProgress(event);
548
+ },
549
+ });
550
+ const evidenceStatus = childResult.exitStatus?.cancelled
551
+ ? "cancelled"
552
+ : childResult.error ||
553
+ (childResult.exitCode && childResult.exitCode !== 0)
554
+ ? "failed"
555
+ : "completed";
556
+ terminalEvidence = [
557
+ ...terminalEvidence,
558
+ {
559
+ operation: "worker",
560
+ status: evidenceStatus,
561
+ startedAt: attemptStartedAt.toISOString(),
562
+ finishedAt: new Date().toISOString(),
563
+ ...(input.signal?.aborted
564
+ ? {
565
+ reason: cancellationReasonFromSignal(
566
+ input.signal,
567
+ ),
568
+ }
569
+ : {}),
570
+ ...(childResult.exitStatus
571
+ ? { exitStatus: childResult.exitStatus }
572
+ : {}),
573
+ },
574
+ ];
575
+ if (evidenceStatus === "cancelled") {
576
+ const cancelReason = input.signal?.aborted
577
+ ? cancellationReasonFromSignal(input.signal)
578
+ : {
579
+ code: "caller_cancelled" as const,
580
+ message: "Worker cancelled.",
581
+ };
582
+ terminalEvidence.push(
583
+ buildSyntheticTerminalEvidence(
584
+ "tool",
585
+ cancelReason,
586
+ attemptStartedAt.toISOString(),
587
+ ),
588
+ );
589
+ appendEvent(manifest.eventsPath, {
590
+ type: "worker.cancelled",
591
+ runId: manifest.runId,
592
+ taskId: task.id,
593
+ message: cancelReason.message,
594
+ data: { terminalEvidence: terminalEvidence.at(-1) },
595
+ });
596
+ }
597
+ startupEvidence = createStartupEvidence({
598
+ command: "pi",
599
+ startedAt: attemptStartedAt,
600
+ finishedAt: new Date(),
601
+ promptSentAt: attemptStartedAt,
602
+ promptAccepted:
603
+ childResult.exitCode === 0 && !childResult.error,
604
+ stderr: childResult.stderr,
605
+ error: childResult.error,
606
+ exitCode: childResult.exitCode,
607
+ });
608
+ exitCode = childResult.exitCode;
609
+ finalStdout = childResult.stdout;
610
+ finalStderr = childResult.stderr;
611
+ // Cap transcript read to MAX_TRANSCRIPT_BYTES to avoid OOM on huge transcripts.
612
+ const MAX_TRANSCRIPT_PARSE_BYTES = 5 * 1024 * 1024;
613
+ const transcriptText = tailReadWithLineSnap(
614
+ transcriptPath,
615
+ MAX_TRANSCRIPT_PARSE_BYTES,
616
+ childResult.stdout,
617
+ );
618
+ parsedOutput = parsePiJsonOutput(transcriptText);
619
+ error =
620
+ childResult.error ||
621
+ (childResult.exitCode && childResult.exitCode !== 0
622
+ ? childResult.stderr ||
623
+ `Child Pi exited with ${childResult.exitCode}`
624
+ : undefined);
625
+ persistHeartbeat(true);
626
+ persistChildProgress({ type: "attempt_finished" }, true);
627
+ const attempt: ModelAttemptSummary = {
628
+ model: model ?? "default",
629
+ success: !error,
630
+ exitCode,
631
+ error,
632
+ };
633
+ modelAttempts.push(attempt);
634
+ task = { ...task, modelAttempts: [...modelAttempts] };
635
+ tasks = updateTask(tasks, task);
636
+ logs.push(
637
+ `MODEL ATTEMPT ${i + 1}: ${attempt.model}`,
638
+ `success=${attempt.success}`,
639
+ `exitCode=${attempt.exitCode ?? "null"}`,
640
+ attempt.error ? `error=${attempt.error}` : "",
641
+ "",
642
+ );
643
+ if (!error) break;
644
+ const nextModel = attemptModels[i + 1];
645
+ if (!nextModel || !isRetryableModelFailure(error)) break;
646
+ logs.push(formatModelAttemptNote(attempt, nextModel), "");
174
647
  }
175
- const summary = progressEventSummary(task, event);
176
- const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
177
- if (decision.shouldAppend) {
178
- // 2.2 caller migration: high-frequency task.progress goes through
179
- // the buffered path; loss-on-kill is acceptable because progress
180
- // is informational and re-derivable from per-agent records.
181
- appendEventFireAndForget(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
182
- lastRunProgressSummary = summary;
183
- lastRunProgressPersistedAt = now;
648
+ // NEW-8 fix: register all attempt transcripts as artifacts, not just the used one.
649
+ // Earlier failed attempts' transcripts exist on disk but were invisible to the artifact system.
650
+ const successfulAttemptIndex = modelAttempts.findIndex(
651
+ (attempt) => attempt.success,
652
+ );
653
+ const usedAttempt =
654
+ successfulAttemptIndex === -1
655
+ ? Math.max(0, modelAttempts.length - 1)
656
+ : successfulAttemptIndex;
657
+ for (
658
+ let attemptIdx = 0;
659
+ attemptIdx < modelAttempts.length;
660
+ attemptIdx++
661
+ ) {
662
+ if (attemptIdx === usedAttempt) continue;
663
+ const tPath = `${manifest.artifactsRoot}/transcripts/${task.id}.attempt-${attemptIdx}.jsonl`;
664
+ if (!fs.existsSync(tPath)) continue;
665
+ const MAX_ATTEMPT_TRANSCRIPT = 5 * 1024 * 1024;
666
+ const tContent = tailReadWithLineSnap(
667
+ tPath,
668
+ MAX_ATTEMPT_TRANSCRIPT,
669
+ "",
670
+ );
671
+ if (tContent) {
672
+ writeArtifact(manifest.artifactsRoot, {
673
+ kind: "log",
674
+ relativePath: `transcripts/${task.id}.attempt-${attemptIdx}.jsonl`,
675
+ content: tContent,
676
+ producer: task.id,
677
+ });
678
+ }
184
679
  }
185
- };
186
- for (let i = 0; i < attemptModels.length; i++) {
187
- const model = attemptModels[i];
188
- const attemptStartedAt = new Date();
189
- const pendingAttempt: ModelAttemptSummary = { model: model ?? "default", success: false };
190
- task = { ...task, modelAttempts: [...modelAttempts, pendingAttempt] };
680
+ resultArtifact = writeArtifact(manifest.artifactsRoot, {
681
+ kind: "result",
682
+ relativePath: `results/${task.id}.txt`,
683
+ content:
684
+ cleanResultText(parsedOutput?.finalText) ??
685
+ cleanResultText(finalStdout) ??
686
+ cleanResultText(finalStderr) ??
687
+ "(no output)",
688
+ producer: task.id,
689
+ });
690
+ logArtifact = writeArtifact(manifest.artifactsRoot, {
691
+ kind: "log",
692
+ relativePath: `logs/${task.id}.log`,
693
+ content: [
694
+ ...logs,
695
+ `finalExitCode=${exitCode ?? "null"}`,
696
+ `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`,
697
+ parsedOutput?.usage
698
+ ? `usage=${JSON.stringify(parsedOutput.usage)}`
699
+ : "",
700
+ "",
701
+ "STDOUT:",
702
+ finalStdout,
703
+ "",
704
+ "STDERR:",
705
+ finalStderr,
706
+ ].join("\n"),
707
+ producer: task.id,
708
+ });
709
+ const resolvedModel =
710
+ modelAttempts[usedAttempt]?.model ?? candidates[0] ?? "default";
711
+ const fallbackReason =
712
+ usedAttempt > 0
713
+ ? modelAttempts[usedAttempt - 1]?.error
714
+ : undefined;
715
+ task = {
716
+ ...task,
717
+ modelRouting: {
718
+ requested: modelRoutingPlan.requested,
719
+ resolved: resolvedModel,
720
+ fallbackChain: candidates,
721
+ reason: fallbackReason ?? modelRoutingPlan.reason,
722
+ usedAttempt,
723
+ },
724
+ };
191
725
  tasks = updateTask(tasks, task);
192
- upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
193
- const childResult = await runChildPi({
194
- cwd: task.cwd,
195
- task: prompt,
726
+ // Use the last attempt's transcript for session usage.
727
+ // Safety net: transcriptPath may be undefined in edge cases (e.g., early exit before loop).
728
+ // In practice it is always set inside the for loop above.
729
+ const attemptFallback = `${manifest.artifactsRoot}/transcripts/${task.id}.attempt-${usedAttempt}.jsonl`;
730
+ const sessionUsage = parseSessionUsage(
731
+ transcriptPath ?? attemptFallback,
732
+ );
733
+ const effectiveUsage = parsedOutput?.usage ?? sessionUsage;
734
+ if (effectiveUsage) {
735
+ parsedOutput = {
736
+ ...(parsedOutput ?? { jsonEvents: 0, textEvents: [] }),
737
+ usage: effectiveUsage,
738
+ };
739
+ task = {
740
+ ...task,
741
+ usage: effectiveUsage,
742
+ agentProgress: applyUsageToProgress(
743
+ task.agentProgress,
744
+ effectiveUsage,
745
+ ),
746
+ };
747
+ tasks = updateTask(tasks, task);
748
+ upsertCrewAgent(
749
+ manifest,
750
+ recordFromTask(manifest, task, "child-process"),
751
+ );
752
+ }
753
+ // M2 fix: use attempt-relative path; cap content at MAX_TRANSCRIPT_ARTIFACT_BYTES.
754
+ const MAX_TRANSCRIPT_ARTIFACT_BYTES = 5 * 1024 * 1024; // 5MB cap
755
+ const attemptTranscriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.attempt-${usedAttempt}.jsonl`;
756
+ const transcriptContent = tailReadWithLineSnap(
757
+ attemptTranscriptPath,
758
+ MAX_TRANSCRIPT_ARTIFACT_BYTES,
759
+ "",
760
+ );
761
+ if (transcriptContent) {
762
+ transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
763
+ kind: "log",
764
+ relativePath: `transcripts/${task.id}.attempt-${usedAttempt}.jsonl`,
765
+ content: transcriptContent,
766
+ producer: task.id,
767
+ });
768
+ }
769
+ task = {
770
+ ...task,
771
+ resultArtifact,
772
+ ...(logArtifact ? { logArtifact } : {}),
773
+ ...(transcriptArtifact ? { transcriptArtifact } : {}),
774
+ };
775
+ tasks = updateTask(tasks, task);
776
+ ({ task, tasks } = checkpointTask(
777
+ manifest,
778
+ tasks,
779
+ task,
780
+ "artifact-written",
781
+ ));
782
+ } else if (runtimeKind === "live-session") {
783
+ // LAZY: live-executor is only needed for live-session runtime branches.
784
+ const { runLiveTask } = await import(
785
+ "./task-runner/live-executor.ts"
786
+ );
787
+ const live = await runLiveTask({
788
+ manifest,
789
+ tasks,
790
+ task,
791
+ step: input.step,
196
792
  agent: input.agent,
197
- model,
793
+ prompt,
198
794
  signal: input.signal,
199
- transcriptPath,
200
- maxDepth: input.limits?.maxTaskDepth,
201
- skillPaths,
202
- onSpawn: (pid) => {
203
- ({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
204
- },
205
- onLifecycleEvent: (event: ChildPiLifecycleEvent) => {
206
- appendEvent(manifest.eventsPath, { type: `worker.${event.type}` as const, runId: manifest.runId, taskId: task.id, message: `Worker lifecycle: ${event.type}${event.error ? ` error=${event.error}` : ""}${event.exitCode != null ? ` exit=${event.exitCode}` : ""}`, data: { ...event } });
207
- },
208
- onStdoutLine: (line) => {
209
- appendCrewAgentOutput(manifest, task.id, line);
210
- persistHeartbeat();
211
- // Check for supervisor contact requests from child Pi
212
- const contact = parseSupervisorContactFromLine(line);
213
- if (contact) {
214
- recordSupervisorContact(manifest, { runId: manifest.runId, ...contact });
215
- }
216
- },
217
- onJsonEvent: (event) => {
218
- appendCrewAgentEvent(manifest, task.id, event);
219
- if (event && typeof event === "object" && !Array.isArray(event)) collectedJsonEvents.push(event as Record<string, unknown>);
220
- persistHeartbeat();
221
- task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
222
- tasks = updateTask(tasks, task);
223
- // Bridge event to UI event bus for near-instant updates
224
- try {
225
- const bridgeEvent = bridgeEventFromJsonEvent(manifest.runId, task.id, event);
226
- if (bridgeEvent) streamBridge?.handler(bridgeEvent);
227
- } catch { /* bridge errors should not affect task */ }
228
- // Feed overflow recovery tracker
229
- if (input.onJsonEvent) {
230
- try {
231
- input.onJsonEvent(task.id, manifest.runId, event);
232
- } catch { /* overflow tracking errors should not affect task */ }
233
- }
234
- if (!finalCheckpointWritten && isFinalChildEvent(event)) {
235
- finalCheckpointWritten = true;
236
- ({ task, tasks } = checkpointTask(manifest, tasks, task, "child-stdout-final"));
237
- }
238
- persistChildProgress(event);
239
- },
795
+ runtimeConfig: input.runtimeConfig,
796
+ parentContext: input.parentContext,
797
+ parentModel: input.parentModel,
798
+ modelRegistry: input.modelRegistry,
799
+ modelOverride: input.modelOverride,
800
+ teamRoleModel: input.teamRoleModel,
801
+ workspaceId: input.workspaceId,
240
802
  });
241
- const evidenceStatus = childResult.exitStatus?.cancelled ? "cancelled" : childResult.error || (childResult.exitCode && childResult.exitCode !== 0) ? "failed" : "completed";
242
- terminalEvidence = [...terminalEvidence, { operation: "worker", status: evidenceStatus, startedAt: attemptStartedAt.toISOString(), finishedAt: new Date().toISOString(), ...(input.signal?.aborted ? { reason: cancellationReasonFromSignal(input.signal) } : {}), ...(childResult.exitStatus ? { exitStatus: childResult.exitStatus } : {}) }];
243
- if (evidenceStatus === "cancelled") {
244
- const cancelReason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : { code: "caller_cancelled" as const, message: "Worker cancelled." };
245
- terminalEvidence.push(buildSyntheticTerminalEvidence("tool", cancelReason, attemptStartedAt.toISOString()));
246
- appendEvent(manifest.eventsPath, { type: "worker.cancelled", runId: manifest.runId, taskId: task.id, message: cancelReason.message, data: { terminalEvidence: terminalEvidence.at(-1) } });
247
- }
248
- 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 });
249
- exitCode = childResult.exitCode;
250
- finalStdout = childResult.stdout;
251
- finalStderr = childResult.stderr;
252
- parsedOutput = parsePiJsonOutput(fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : childResult.stdout);
253
- error = childResult.error || (childResult.exitCode && childResult.exitCode !== 0 ? childResult.stderr || `Child Pi exited with ${childResult.exitCode}` : undefined);
254
- persistHeartbeat(true);
255
- persistChildProgress({ type: "attempt_finished" }, true);
256
- const attempt: ModelAttemptSummary = { model: model ?? "default", success: !error, exitCode, error };
257
- modelAttempts.push(attempt);
258
- task = { ...task, modelAttempts: [...modelAttempts] };
259
- tasks = updateTask(tasks, task);
260
- logs.push(`MODEL ATTEMPT ${i + 1}: ${attempt.model}`, `success=${attempt.success}`, `exitCode=${attempt.exitCode ?? "null"}`, attempt.error ? `error=${attempt.error}` : "", "");
261
- if (!error) break;
262
- const nextModel = attemptModels[i + 1];
263
- if (!nextModel || !isRetryableModelFailure(error)) break;
264
- logs.push(formatModelAttemptNote(attempt, nextModel), "");
265
- }
266
- resultArtifact = writeArtifact(manifest.artifactsRoot, {
267
- kind: "result",
268
- relativePath: `results/${task.id}.txt`,
269
- content: cleanResultText(parsedOutput?.finalText) ?? cleanResultText(finalStdout) ?? cleanResultText(finalStderr) ?? "(no output)",
270
- producer: task.id,
271
- });
272
- logArtifact = writeArtifact(manifest.artifactsRoot, {
273
- kind: "log",
274
- relativePath: `logs/${task.id}.log`,
275
- content: [...logs, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`, parsedOutput?.usage ? `usage=${JSON.stringify(parsedOutput.usage)}` : "", "", "STDOUT:", finalStdout, "", "STDERR:", finalStderr].join("\n"),
276
- producer: task.id,
277
- });
278
- const successfulAttemptIndex = modelAttempts.findIndex((attempt) => attempt.success);
279
- const usedAttempt = successfulAttemptIndex === -1 ? Math.max(0, modelAttempts.length - 1) : successfulAttemptIndex;
280
- const resolvedModel = modelAttempts[usedAttempt]?.model ?? candidates[0] ?? "default";
281
- const fallbackReason = usedAttempt > 0 ? modelAttempts[usedAttempt - 1]?.error : undefined;
282
- task = { ...task, modelRouting: { requested: modelRoutingPlan.requested, resolved: resolvedModel, fallbackChain: candidates, reason: fallbackReason ?? modelRoutingPlan.reason, usedAttempt } };
283
- tasks = updateTask(tasks, task);
284
- const sessionUsage = parseSessionUsage(transcriptPath);
285
- const effectiveUsage = parsedOutput?.usage ?? sessionUsage;
286
- if (effectiveUsage) {
287
- parsedOutput = { ...(parsedOutput ?? { jsonEvents: 0, textEvents: [] }), usage: effectiveUsage };
288
- task = { ...task, usage: effectiveUsage, agentProgress: applyUsageToProgress(task.agentProgress, effectiveUsage) };
289
- tasks = updateTask(tasks, task);
290
- upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
291
- }
292
- if (fs.existsSync(transcriptPath)) {
293
- transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
294
- kind: "log",
295
- relativePath: `transcripts/${task.id}.jsonl`,
296
- content: fs.readFileSync(transcriptPath, "utf-8"),
803
+ task = live.task;
804
+ tasks = live.tasks;
805
+ startupEvidence = live.startupEvidence;
806
+ exitCode = live.exitCode;
807
+ error = live.error;
808
+ parsedOutput = live.parsedOutput;
809
+ resultArtifact = live.resultArtifact;
810
+ logArtifact = live.logArtifact;
811
+ transcriptArtifact = live.transcriptArtifact;
812
+ } else {
813
+ resultArtifact = writeArtifact(manifest.artifactsRoot, {
814
+ kind: "result",
815
+ relativePath: `results/${task.id}.md`,
816
+ content: [
817
+ `# ${task.id}`,
818
+ "",
819
+ "Worker execution is disabled in this scaffold-safe run.",
820
+ "The prompt artifact contains the exact task that will be sent to a child Pi worker when execution is enabled.",
821
+ ].join("\n"),
297
822
  producer: task.id,
298
823
  });
299
824
  }
300
- task = { ...task, resultArtifact, ...(logArtifact ? { logArtifact } : {}), ...(transcriptArtifact ? { transcriptArtifact } : {}) };
301
- tasks = updateTask(tasks, task);
302
- ({ task, tasks } = checkpointTask(manifest, tasks, task, "artifact-written"));
303
- } else if (runtimeKind === "live-session") {
304
- // LAZY: live-executor is only needed for live-session runtime branches.
305
- const { runLiveTask } = await import("./task-runner/live-executor.ts");
306
- const live = await runLiveTask({ manifest, tasks, task, step: input.step, agent: input.agent, prompt, signal: input.signal, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, teamRoleModel: input.teamRoleModel, workspaceId: input.workspaceId });
307
- task = live.task;
308
- tasks = live.tasks;
309
- startupEvidence = live.startupEvidence;
310
- exitCode = live.exitCode;
311
- error = live.error;
312
- parsedOutput = live.parsedOutput;
313
- resultArtifact = live.resultArtifact;
314
- logArtifact = live.logArtifact;
315
- transcriptArtifact = live.transcriptArtifact;
316
- } else {
317
- resultArtifact = writeArtifact(manifest.artifactsRoot, {
318
- kind: "result",
319
- relativePath: `results/${task.id}.md`,
320
- content: [
321
- `# ${task.id}`,
322
- "",
323
- "Worker execution is disabled in this scaffold-safe run.",
324
- "The prompt artifact contains the exact task that will be sent to a child Pi worker when execution is enabled.",
325
- ].join("\n"),
326
- producer: task.id,
327
- });
328
- }
329
825
 
330
- // --- Yield-based completion contract ---
331
- let yieldResult: YieldResult | undefined;
332
- const yieldEnabled = input.runtimeConfig?.yield?.enabled ?? DEFAULT_YIELD_CONFIG.enabled;
333
- if (yieldEnabled && collectedJsonEvents.length > 0) {
334
- if (hasYieldInOutput(collectedJsonEvents)) {
335
- const yieldEvent = collectedJsonEvents.find((e) => isYieldEvent(e));
336
- if (yieldEvent) {
337
- yieldResult = extractYieldResult(yieldEvent);
826
+ // --- Yield-based completion contract ---
827
+ // _yieldResult: preserved for future use — yield completion contract not yet wired to task.result
828
+ let _yieldResult: YieldResult | undefined;
829
+ let noYield = false;
830
+ const yieldEnabled =
831
+ input.runtimeConfig?.yield?.enabled ?? DEFAULT_YIELD_CONFIG.enabled;
832
+ if (yieldEnabled && collectedJsonEvents.length > 0) {
833
+ if (hasYieldInOutput(collectedJsonEvents)) {
834
+ const yieldEvent = collectedJsonEvents.find((e) =>
835
+ isYieldEvent(e),
836
+ );
837
+ if (yieldEvent) {
838
+ _yieldResult = extractYieldResult(yieldEvent);
839
+ }
840
+ } else if (!error) {
841
+ noYield = true;
842
+ appendEvent(manifest.eventsPath, {
843
+ type: "task.needs_attention",
844
+ runId: manifest.runId,
845
+ taskId: task.id,
846
+ message:
847
+ "Worker completed without calling submit_result tool.",
848
+ data: {
849
+ activityState: "needs_attention",
850
+ reason: "no_yield",
851
+ },
852
+ });
338
853
  }
339
- } else if (!error) {
340
- appendEvent(manifest.eventsPath, { type: "task.attention", runId: manifest.runId, taskId: task.id, message: "Worker completed without calling submit_result tool.", data: { activityState: "needs_attention", reason: "no_yield" } });
341
854
  }
342
- }
343
855
 
344
- const diffArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
345
- kind: "diff",
346
- relativePath: `diffs/${task.id}.diff`,
347
- content: captureWorktreeDiff(workspace.worktreePath),
348
- producer: task.id,
349
- }) : undefined;
350
- const diffStatArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
351
- kind: "metadata",
352
- relativePath: `metadata/${task.id}.diff-stat.json`,
353
- content: `${JSON.stringify({ ...captureWorktreeDiffStat(workspace.worktreePath), syntheticPaths: workspace.syntheticPaths ?? [], nodeModulesLinked: workspace.nodeModulesLinked ?? false }, null, 2)}\n`,
354
- producer: task.id,
355
- }) : undefined;
856
+ const diffArtifact = workspace.worktreePath
857
+ ? writeArtifact(manifest.artifactsRoot, {
858
+ kind: "diff",
859
+ relativePath: `diffs/${task.id}.diff`,
860
+ content: captureWorktreeDiff(workspace.worktreePath),
861
+ producer: task.id,
862
+ })
863
+ : undefined;
864
+ const diffStatArtifact = workspace.worktreePath
865
+ ? writeArtifact(manifest.artifactsRoot, {
866
+ kind: "metadata",
867
+ relativePath: `metadata/${task.id}.diff-stat.json`,
868
+ content: `${JSON.stringify({ ...captureWorktreeDiffStat(workspace.worktreePath), syntheticPaths: workspace.syntheticPaths ?? [], nodeModulesLinked: workspace.nodeModulesLinked ?? false }, null, 2)}\n`,
869
+ producer: task.id,
870
+ })
871
+ : undefined;
356
872
 
357
- const mutationGuardMode = input.runtimeConfig?.completionMutationGuard ?? "warn";
358
- const mutationGuard = !error && mutationGuardMode !== "off" ? evaluateCompletionMutationGuard({ role: task.role, taskText: `${task.title}\n${input.step.task}`, transcriptPath: runtimeKind === "child-process" ? transcriptPath : transcriptArtifact?.path, stdout: finalStdout }) : undefined;
359
- if (mutationGuard?.reason === "no_mutation_observed") {
360
- appendTaskAttentionEvent({
361
- manifest,
362
- taskId: task.id,
363
- message: "Implementation-style task completed without an observed mutation tool call.",
364
- data: { activityState: "needs_attention", reason: "completion_guard", taskId: task.id, agentName: task.agent, observedTools: mutationGuard.observedTools, suggestedAction: mutationGuardMode === "fail" ? "Review the worker output and rerun with a concrete implementation task." : "Review the worker output; set runtime.completionMutationGuard='fail' to enforce this." },
365
- });
366
- task = { ...task, agentProgress: { ...(task.agentProgress ?? emptyCrewAgentProgress()), activityState: "needs_attention" } };
367
- if (mutationGuardMode === "fail") {
368
- error = "Completion mutation guard failed: implementation-style task completed without an observed mutation tool call.";
369
- exitCode = exitCode === 0 ? 1 : exitCode;
370
- if (modelAttempts?.length) {
371
- modelAttempts = modelAttempts.map((attempt, index) => index === modelAttempts!.length - 1 ? { ...attempt, success: false, exitCode, error } : attempt);
873
+ const mutationGuardMode =
874
+ input.runtimeConfig?.completionMutationGuard ?? "warn";
875
+ const mutationGuard =
876
+ !error && mutationGuardMode !== "off"
877
+ ? evaluateCompletionMutationGuard({
878
+ role: task.role,
879
+ taskText: `${task.title}\n${input.step.task}`,
880
+ transcriptPath:
881
+ runtimeKind === "child-process"
882
+ ? transcriptPath
883
+ : transcriptArtifact?.path,
884
+ stdout: finalStdout,
885
+ })
886
+ : undefined;
887
+ if (mutationGuard?.reason === "no_mutation_observed") {
888
+ appendTaskAttentionEvent({
889
+ manifest,
890
+ taskId: task.id,
891
+ message:
892
+ "Implementation-style task completed without an observed mutation tool call.",
893
+ data: {
894
+ activityState: "needs_attention",
895
+ reason: "completion_guard",
896
+ taskId: task.id,
897
+ agentName: task.agent,
898
+ observedTools: mutationGuard.observedTools,
899
+ suggestedAction:
900
+ mutationGuardMode === "fail"
901
+ ? "Review the worker output and rerun with a concrete implementation task."
902
+ : "Review the worker output; set runtime.completionMutationGuard='fail' to enforce this.",
903
+ },
904
+ });
905
+ task = {
906
+ ...task,
907
+ agentProgress: {
908
+ ...(task.agentProgress ?? emptyCrewAgentProgress()),
909
+ activityState: "needs_attention",
910
+ },
911
+ };
912
+ if (mutationGuardMode === "fail") {
913
+ error =
914
+ "Completion mutation guard failed: implementation-style task completed without an observed mutation tool call.";
915
+ exitCode = exitCode === 0 ? 1 : exitCode;
916
+ if (modelAttempts?.length) {
917
+ modelAttempts = modelAttempts.map((attempt, index) =>
918
+ index === modelAttempts!.length - 1
919
+ ? { ...attempt, success: false, exitCode, error }
920
+ : attempt,
921
+ );
922
+ }
372
923
  }
924
+ tasks = updateTask(tasks, task);
373
925
  }
374
- tasks = updateTask(tasks, task);
375
- }
376
926
 
377
- // --- Output format validation (caveman Phase 4) ---
378
- // Validate worker output against the role's output contract.
379
- // On failure: emit attention event but don't fail the task.
380
- let outputValidation: OutputValidationResult | undefined;
381
- if (!error) {
382
- const outputText = parsedOutput?.finalText ?? finalStdout;
383
- if (outputText) {
384
- outputValidation = validateWorkerOutput(task.role, outputText);
385
- if (!outputValidation.valid) {
386
- appendEvent(manifest.eventsPath, { type: "task.output_validation", runId: manifest.runId, taskId: task.id, data: { valid: false, formatMatch: outputValidation.formatMatch, structurePreserved: outputValidation.structurePreserved, issues: outputValidation.issues } });
387
- task = { ...task, agentProgress: { ...(task.agentProgress ?? emptyCrewAgentProgress()), activityState: "needs_attention" } };
388
- tasks = updateTask(tasks, task);
927
+ // --- Output format validation (caveman Phase 4) ---
928
+ // Validate worker output against the role's output contract.
929
+ // On failure: emit attention event but don't fail the task.
930
+ let outputValidation: OutputValidationResult | undefined;
931
+ if (!error) {
932
+ const outputText = parsedOutput?.finalText ?? finalStdout;
933
+ if (outputText) {
934
+ outputValidation = validateWorkerOutput(task.role, outputText);
935
+ if (!outputValidation.valid) {
936
+ appendEvent(manifest.eventsPath, {
937
+ type: "task.output_validation",
938
+ runId: manifest.runId,
939
+ taskId: task.id,
940
+ data: {
941
+ valid: false,
942
+ formatMatch: outputValidation.formatMatch,
943
+ structurePreserved:
944
+ outputValidation.structurePreserved,
945
+ issues: outputValidation.issues,
946
+ },
947
+ });
948
+ task = {
949
+ ...task,
950
+ agentProgress: {
951
+ ...(task.agentProgress ?? emptyCrewAgentProgress()),
952
+ activityState: "needs_attention",
953
+ },
954
+ };
955
+ tasks = updateTask(tasks, task);
956
+ }
389
957
  }
390
958
  }
391
- }
392
959
 
393
- task = {
394
- ...task,
395
- status: error ? "failed" : "completed",
396
- finishedAt: new Date().toISOString(),
397
- exitCode,
398
- modelAttempts,
399
- usage: parsedOutput?.usage,
400
- jsonEvents: parsedOutput?.jsonEvents,
401
- agentProgress: error && task.agentProgress?.currentTool ? { ...task.agentProgress, failedTool: task.agentProgress.currentTool } : task.agentProgress,
402
- error,
403
- 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.`),
404
- promptArtifact,
405
- resultArtifact,
406
- claim: undefined,
407
- heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
408
- workerExitStatus: terminalEvidence.at(-1)?.exitStatus,
409
- terminalEvidence: terminalEvidence.length ? [...(task.terminalEvidence ?? []), ...terminalEvidence] : task.terminalEvidence,
410
- ...(logArtifact ? { logArtifact } : {}),
411
- ...(transcriptArtifact ? { transcriptArtifact } : {}),
412
- };
413
- tasks = updateTask(tasks, task);
414
- const packetArtifact = writeArtifact(manifest.artifactsRoot, {
415
- kind: "metadata",
416
- relativePath: `metadata/${task.id}.task-packet.json`,
417
- content: `${JSON.stringify(task.taskPacket, null, 2)}\n`,
418
- producer: task.id,
419
- });
420
- const verificationArtifact = writeArtifact(manifest.artifactsRoot, {
421
- kind: "metadata",
422
- relativePath: `metadata/${task.id}.verification.json`,
423
- content: `${JSON.stringify(task.verification, null, 2)}\n`,
424
- producer: task.id,
425
- });
426
- const sharedOutputArtifact = writeTaskSharedOutput(manifest, input.step, task);
427
- const startupArtifact = writeArtifact(manifest.artifactsRoot, {
428
- kind: "metadata",
429
- relativePath: `metadata/${task.id}.startup-evidence.json`,
430
- content: `${JSON.stringify(startupEvidence, null, 2)}\n`,
431
- producer: task.id,
432
- });
433
- const permissionArtifact = writeArtifact(manifest.artifactsRoot, {
434
- kind: "metadata",
435
- relativePath: `metadata/${task.id}.permission.json`,
436
- content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
437
- producer: task.id,
438
- });
439
- const capabilityArtifact = writeArtifact(manifest.artifactsRoot, {
440
- kind: "metadata",
441
- relativePath: `metadata/${task.id}.capabilities.json`,
442
- content: `${JSON.stringify(buildWorkerCapabilityInventory({ taskId: task.id, role: task.role, agent: input.agent, runtime: runtimeKind, permissionMode, skillNames, skillPaths, skillsDisabled: input.skillOverride === false || input.teamRoleSkills === false, modelOverride: input.modelOverride, teamRoleModel: input.teamRoleModel, stepModel: input.step.model }), null, 2)}\n`,
443
- producer: task.id,
444
- });
445
- const promptPipelineArtifact = writeArtifact(manifest.artifactsRoot, {
446
- kind: "metadata",
447
- relativePath: `metadata/${task.id}.prompt-pipeline.json`,
448
- content: `${JSON.stringify(buildWorkerPromptPipeline({ artifactsRoot: manifest.artifactsRoot, taskId: task.id, promptArtifact, inputsArtifact, skillArtifact, capabilityArtifact, coordinationArtifact, skillInstructionCount: skillNames?.length ?? 0, skillsDisabled: input.skillOverride === false || input.teamRoleSkills === false }), null, 2)}\n`,
449
- producer: task.id,
450
- });
451
- const outputValidationArtifact = outputValidation ? writeArtifact(manifest.artifactsRoot, {
452
- kind: "metadata",
453
- relativePath: `metadata/${task.id}.output-validation.json`,
454
- content: `${JSON.stringify(outputValidation, null, 2)}\n`,
455
- producer: task.id,
456
- }) : undefined;
457
- manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, ...(skillArtifact ? [skillArtifact] : []), packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, capabilityArtifact, promptPipelineArtifact, ...(outputValidationArtifact ? [outputValidationArtifact] : []), ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
458
- saveRunManifest(manifest);
459
- tasks = persistSingleTaskUpdate(manifest, tasks, task);
460
- upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
461
- // Execute task_result hook before emitting terminal event
462
- const hookReport = await executeHook("task_result", { runId: manifest.runId, taskId: task.id, cwd: manifest.cwd });
463
- appendHookEvent(manifest, hookReport);
464
- appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
465
- return { manifest, tasks };
960
+ task = {
961
+ ...task,
962
+ status: error ? "failed" : noYield ? "needs_attention" : "completed",
963
+ finishedAt: new Date().toISOString(),
964
+ exitCode,
965
+ modelAttempts,
966
+ usage: parsedOutput?.usage,
967
+ jsonEvents: parsedOutput?.jsonEvents,
968
+ agentProgress:
969
+ error && task.agentProgress?.currentTool
970
+ ? {
971
+ ...task.agentProgress,
972
+ failedTool: task.agentProgress.currentTool,
973
+ }
974
+ : task.agentProgress,
975
+ error,
976
+ verification: createVerificationEvidence(
977
+ taskPacket.verification,
978
+ !error,
979
+ error
980
+ ? `Task failed: ${error}`
981
+ : runtimeKind === "scaffold"
982
+ ? "Safe scaffold mode; verification commands were not executed."
983
+ : `${runtimeKind} worker finished without reporting a verification failure.`,
984
+ ),
985
+ promptArtifact,
986
+ resultArtifact,
987
+ claim: undefined,
988
+ heartbeat: touchWorkerHeartbeat(
989
+ task.heartbeat ?? createWorkerHeartbeat(task.id),
990
+ { alive: false },
991
+ ),
992
+ workerExitStatus: terminalEvidence.at(-1)?.exitStatus,
993
+ terminalEvidence: terminalEvidence.length
994
+ ? [...(task.terminalEvidence ?? []), ...terminalEvidence]
995
+ : task.terminalEvidence,
996
+ ...(logArtifact ? { logArtifact } : {}),
997
+ ...(transcriptArtifact ? { transcriptArtifact } : {}),
998
+ };
999
+ tasks = updateTask(tasks, task);
1000
+ const packetArtifact = writeArtifact(manifest.artifactsRoot, {
1001
+ kind: "metadata",
1002
+ relativePath: `metadata/${task.id}.task-packet.json`,
1003
+ content: `${JSON.stringify(task.taskPacket, null, 2)}\n`,
1004
+ producer: task.id,
1005
+ });
1006
+ const verificationArtifact = writeArtifact(manifest.artifactsRoot, {
1007
+ kind: "metadata",
1008
+ relativePath: `metadata/${task.id}.verification.json`,
1009
+ content: `${JSON.stringify(task.verification, null, 2)}\n`,
1010
+ producer: task.id,
1011
+ });
1012
+ const sharedOutputArtifact = writeTaskSharedOutput(
1013
+ manifest,
1014
+ input.step,
1015
+ task,
1016
+ );
1017
+ const startupArtifact = writeArtifact(manifest.artifactsRoot, {
1018
+ kind: "metadata",
1019
+ relativePath: `metadata/${task.id}.startup-evidence.json`,
1020
+ content: `${JSON.stringify(startupEvidence, null, 2)}\n`,
1021
+ producer: task.id,
1022
+ });
1023
+ const permissionArtifact = writeArtifact(manifest.artifactsRoot, {
1024
+ kind: "metadata",
1025
+ relativePath: `metadata/${task.id}.permission.json`,
1026
+ content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
1027
+ producer: task.id,
1028
+ });
1029
+ const capabilityArtifact = writeArtifact(manifest.artifactsRoot, {
1030
+ kind: "metadata",
1031
+ relativePath: `metadata/${task.id}.capabilities.json`,
1032
+ content: `${JSON.stringify(buildWorkerCapabilityInventory({ taskId: task.id, role: task.role, agent: input.agent, runtime: runtimeKind, permissionMode, skillNames, skillPaths, skillsDisabled: input.skillOverride === false || input.teamRoleSkills === false, modelOverride: input.modelOverride, teamRoleModel: input.teamRoleModel, stepModel: input.step.model }), null, 2)}\n`,
1033
+ producer: task.id,
1034
+ });
1035
+ const promptPipelineArtifact = writeArtifact(manifest.artifactsRoot, {
1036
+ kind: "metadata",
1037
+ relativePath: `metadata/${task.id}.prompt-pipeline.json`,
1038
+ content: `${JSON.stringify(buildWorkerPromptPipeline({ artifactsRoot: manifest.artifactsRoot, taskId: task.id, promptArtifact, inputsArtifact, skillArtifact, capabilityArtifact, coordinationArtifact, skillInstructionCount: skillNames?.length ?? 0, skillsDisabled: input.skillOverride === false || input.teamRoleSkills === false }), null, 2)}\n`,
1039
+ producer: task.id,
1040
+ });
1041
+ const outputValidationArtifact = outputValidation
1042
+ ? writeArtifact(manifest.artifactsRoot, {
1043
+ kind: "metadata",
1044
+ relativePath: `metadata/${task.id}.output-validation.json`,
1045
+ content: `${JSON.stringify(outputValidation, null, 2)}\n`,
1046
+ producer: task.id,
1047
+ })
1048
+ : undefined;
1049
+ manifest = {
1050
+ ...manifest,
1051
+ updatedAt: new Date().toISOString(),
1052
+ artifacts: [
1053
+ ...manifest.artifacts,
1054
+ promptArtifact,
1055
+ resultArtifact,
1056
+ inputsArtifact,
1057
+ coordinationArtifact,
1058
+ ...(skillArtifact ? [skillArtifact] : []),
1059
+ packetArtifact,
1060
+ verificationArtifact,
1061
+ startupArtifact,
1062
+ permissionArtifact,
1063
+ capabilityArtifact,
1064
+ promptPipelineArtifact,
1065
+ ...(outputValidationArtifact ? [outputValidationArtifact] : []),
1066
+ ...(sharedOutputArtifact ? [sharedOutputArtifact] : []),
1067
+ ...(logArtifact ? [logArtifact] : []),
1068
+ ...(transcriptArtifact ? [transcriptArtifact] : []),
1069
+ ...(diffArtifact ? [diffArtifact] : []),
1070
+ ...(diffStatArtifact ? [diffStatArtifact] : []),
1071
+ ],
1072
+ };
1073
+ saveRunManifest(manifest);
1074
+ tasks = persistSingleTaskUpdate(manifest, tasks, task);
1075
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
1076
+ // Execute task_result hook before emitting terminal event
1077
+ const hookReport = await executeHook("task_result", {
1078
+ runId: manifest.runId,
1079
+ taskId: task.id,
1080
+ cwd: manifest.cwd,
1081
+ });
1082
+ appendHookEvent(manifest, hookReport);
1083
+ appendEvent(manifest.eventsPath, {
1084
+ type: error ? "task.failed" : noYield ? "task.needs_attention" : "task.completed",
1085
+ runId: manifest.runId,
1086
+ taskId: task.id,
1087
+ message: error,
1088
+ });
1089
+ return { manifest, tasks };
466
1090
  } finally {
467
1091
  streamBridge?.dispose();
468
1092
  }