pi-crew 0.2.20 → 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -10
- package/README.md +4 -2
- package/docs/PROJECT_REVIEW.md +271 -0
- package/docs/PROJECT_REVIEW_FIXES.md +343 -0
- package/docs/PROJECT_REVIEW_ROUND4.md +156 -0
- package/docs/PROJECT_REVIEW_ROUND5.md +86 -0
- package/docs/fixes/BATCH_A_H1_H2.md +86 -0
- package/docs/fixes/bug-006-foreground-cancel-concurrent.md +78 -0
- package/docs/fixes/bug-007-async-notifier-stale-ctx.md +112 -0
- package/docs/fixes/bug-008-child-process-silent-timeout.md +100 -0
- package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +75 -0
- package/docs/fixes/bug-010-child-process-api-key-filtered.md +109 -0
- package/docs/fixes/bug-011-spawn-pi-enoent.md +92 -0
- package/docs/fixes/bug-012-essential-env-stripped.md +89 -0
- package/docs/fixes/bug-013-background-runner-death.md +84 -0
- package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +82 -0
- package/docs/fixes/bug-015-background-runner-sigterm.md +65 -0
- package/docs/fixes/bug-017-background-runner-session-shutdown.md +66 -0
- package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +28 -0
- package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +61 -0
- package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +52 -0
- package/docs/pi-crew-bugs.md +954 -0
- package/docs/pi-crew-investigation-report.md +411 -0
- package/docs/pi-crew-test-final.md +120 -0
- package/docs/pi-crew-test-results.md +260 -0
- package/docs/pi-crew-test-round2.md +136 -0
- package/docs/pi-crew-test-round4.md +100 -0
- package/docs/pi-crew-test-round5.md +70 -0
- package/docs/pi-crew-test-round6.md +110 -0
- package/docs/usage.md +14 -0
- package/package.json +4 -2
- package/src/adapters/export-util.ts +12 -6
- package/src/agents/agent-config.ts +2 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/markers.ts +22 -17
- package/src/config/resilient-parser.ts +1 -1
- package/src/extension/async-notifier.ts +4 -2
- package/src/extension/management.ts +52 -0
- package/src/extension/register.ts +47 -10
- package/src/extension/run-index.ts +20 -2
- package/src/extension/run-maintenance.ts +2 -2
- package/src/extension/team-tool/parallel-dispatch.ts +1 -1
- package/src/extension/team-tool/run.ts +3 -6
- package/src/extension/team-tool.ts +67 -11
- package/src/observability/event-to-metric.ts +2 -1
- package/src/runtime/async-runner.ts +42 -34
- package/src/runtime/background-runner.ts +165 -7
- package/src/runtime/child-pi.ts +111 -18
- package/src/runtime/code-summary.ts +1 -1
- package/src/runtime/crash-recovery.ts +1 -1
- package/src/runtime/crew-agent-runtime.ts +2 -1
- package/src/runtime/heartbeat-watcher.ts +4 -0
- package/src/runtime/live-agent-manager.ts +1 -1
- package/src/runtime/live-session-runtime.ts +2 -1
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +2 -1
- package/src/runtime/phase-progress.ts +1 -1
- package/src/runtime/pi-args.ts +3 -1
- package/src/runtime/pi-spawn.ts +6 -0
- package/src/runtime/prose-compressor.ts +1 -1
- package/src/runtime/result-extractor.ts +0 -1
- package/src/runtime/retry-executor.ts +1 -1
- package/src/runtime/runtime-resolver.ts +8 -3
- package/src/runtime/skill-instructions.ts +0 -1
- package/src/runtime/stale-reconciler.ts +30 -3
- package/src/runtime/subagent-manager.ts +2 -0
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-graph-scheduler.ts +1 -1
- package/src/runtime/task-runner/live-executor.ts +15 -0
- package/src/runtime/task-runner/tail-read.ts +26 -0
- package/src/runtime/task-runner.ts +1007 -383
- package/src/runtime/team-runner.ts +9 -5
- package/src/runtime/worker-startup.ts +3 -1
- package/src/schema/team-tool-schema.ts +2 -1
- package/src/state/active-run-registry.ts +8 -2
- package/src/state/atomic-write.ts +17 -0
- package/src/state/contracts.ts +5 -2
- package/src/state/event-log-rotation.ts +118 -31
- package/src/state/event-log.ts +33 -5
- package/src/state/event-reconstructor.ts +4 -2
- package/src/state/mailbox.ts +5 -1
- package/src/state/schedule.ts +146 -0
- package/src/state/types.ts +40 -0
- package/src/state/usage.ts +20 -0
- package/src/ui/crew-widget.ts +2 -2
- package/src/ui/run-event-bus.ts +1 -1
- package/src/ui/run-snapshot-cache.ts +2 -1
- package/src/ui/snapshot-types.ts +1 -0
- package/src/utils/gh-protocol.ts +2 -2
- package/src/utils/names.ts +1 -1
- package/src/utils/sse-parser.ts +0 -2
- package/src/worktree/branch-freshness.ts +1 -1
- package/src/worktree/cleanup.ts +54 -14
- 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 {
|
|
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 {
|
|
16
|
+
import {
|
|
17
|
+
createWorkerHeartbeat,
|
|
18
|
+
touchWorkerHeartbeat,
|
|
19
|
+
} from "./worker-heartbeat.ts";
|
|
10
20
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
|
21
|
-
|
|
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 {
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
30
|
-
|
|
31
|
-
|
|
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 {
|
|
88
|
+
import {
|
|
89
|
+
cancellationReasonFromSignal,
|
|
90
|
+
buildSyntheticTerminalEvidence,
|
|
91
|
+
} from "./cancellation.ts";
|
|
34
92
|
import { appendTaskAttentionEvent } from "./attention-events.ts";
|
|
35
|
-
import {
|
|
36
|
-
|
|
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 {
|
|
39
|
-
|
|
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(
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
taskPacket
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
793
|
+
prompt,
|
|
198
794
|
signal: input.signal,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
}
|