pi-cohort 2.0.0

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 (92) hide show
  1. package/CHANGELOG.md +1151 -0
  2. package/LICENSE +22 -0
  3. package/README.md +1220 -0
  4. package/agents/context-builder.md +45 -0
  5. package/agents/delegate.md +12 -0
  6. package/agents/oracle.md +73 -0
  7. package/agents/planner.md +55 -0
  8. package/agents/reviewer.md +91 -0
  9. package/agents/scout.md +50 -0
  10. package/agents/worker.md +67 -0
  11. package/package.json +87 -0
  12. package/prompts/gather-context-and-clarify.md +13 -0
  13. package/prompts/parallel-cleanup.md +59 -0
  14. package/prompts/parallel-context-build.md +55 -0
  15. package/prompts/parallel-handoff-plan.md +61 -0
  16. package/prompts/parallel-review.md +54 -0
  17. package/prompts/review-loop.md +41 -0
  18. package/skills/pi-cohort/SKILL.md +818 -0
  19. package/src/agents/agent-management.ts +685 -0
  20. package/src/agents/agent-scope.ts +6 -0
  21. package/src/agents/agent-selection.ts +23 -0
  22. package/src/agents/agent-serializer.ts +83 -0
  23. package/src/agents/agents.ts +1141 -0
  24. package/src/agents/chain-serializer.ts +251 -0
  25. package/src/agents/frontmatter.ts +29 -0
  26. package/src/agents/identity.ts +30 -0
  27. package/src/agents/skills.ts +632 -0
  28. package/src/extension/config.ts +16 -0
  29. package/src/extension/control-notices.ts +92 -0
  30. package/src/extension/doctor.ts +236 -0
  31. package/src/extension/fanout-child.ts +170 -0
  32. package/src/extension/grand-total.ts +109 -0
  33. package/src/extension/index.ts +630 -0
  34. package/src/extension/schemas.ts +306 -0
  35. package/src/intercom/intercom-bridge.ts +379 -0
  36. package/src/intercom/result-intercom.ts +377 -0
  37. package/src/runs/background/async-execution.ts +796 -0
  38. package/src/runs/background/async-job-tracker.ts +320 -0
  39. package/src/runs/background/async-resume.ts +345 -0
  40. package/src/runs/background/async-status.ts +335 -0
  41. package/src/runs/background/completion-dedupe.ts +63 -0
  42. package/src/runs/background/notify.ts +108 -0
  43. package/src/runs/background/parallel-groups.ts +45 -0
  44. package/src/runs/background/result-watcher.ts +307 -0
  45. package/src/runs/background/run-id-resolver.ts +83 -0
  46. package/src/runs/background/run-status.ts +272 -0
  47. package/src/runs/background/stale-run-reconciler.ts +336 -0
  48. package/src/runs/background/subagent-runner.ts +2326 -0
  49. package/src/runs/background/top-level-async.ts +13 -0
  50. package/src/runs/foreground/chain-clarify.ts +1333 -0
  51. package/src/runs/foreground/chain-execution.ts +1187 -0
  52. package/src/runs/foreground/execution.ts +1028 -0
  53. package/src/runs/foreground/subagent-executor.ts +2580 -0
  54. package/src/runs/shared/acceptance.ts +605 -0
  55. package/src/runs/shared/chain-outputs.ts +101 -0
  56. package/src/runs/shared/completion-guard.ts +143 -0
  57. package/src/runs/shared/dynamic-fanout.ts +293 -0
  58. package/src/runs/shared/long-running-guard.ts +175 -0
  59. package/src/runs/shared/model-fallback.ts +103 -0
  60. package/src/runs/shared/nested-events.ts +822 -0
  61. package/src/runs/shared/nested-path.ts +52 -0
  62. package/src/runs/shared/nested-render.ts +115 -0
  63. package/src/runs/shared/parallel-utils.ts +136 -0
  64. package/src/runs/shared/pi-args.ts +221 -0
  65. package/src/runs/shared/pi-spawn.ts +115 -0
  66. package/src/runs/shared/run-history.ts +60 -0
  67. package/src/runs/shared/single-output.ts +164 -0
  68. package/src/runs/shared/structured-output.ts +77 -0
  69. package/src/runs/shared/subagent-control.ts +287 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +220 -0
  71. package/src/runs/shared/workflow-graph.ts +206 -0
  72. package/src/runs/shared/worktree.ts +577 -0
  73. package/src/shared/artifacts.ts +98 -0
  74. package/src/shared/atomic-json.ts +16 -0
  75. package/src/shared/file-coalescer.ts +40 -0
  76. package/src/shared/fork-context.ts +76 -0
  77. package/src/shared/formatters.ts +133 -0
  78. package/src/shared/jsonl-writer.ts +81 -0
  79. package/src/shared/model-info.ts +78 -0
  80. package/src/shared/post-exit-stdio-guard.ts +85 -0
  81. package/src/shared/session-identity.ts +10 -0
  82. package/src/shared/session-tokens.ts +46 -0
  83. package/src/shared/settings.ts +447 -0
  84. package/src/shared/status-format.ts +59 -0
  85. package/src/shared/types.ts +1072 -0
  86. package/src/shared/utils.ts +451 -0
  87. package/src/slash/prompt-template-bridge.ts +397 -0
  88. package/src/slash/slash-bridge.ts +174 -0
  89. package/src/slash/slash-commands.ts +567 -0
  90. package/src/slash/slash-live-state.ts +292 -0
  91. package/src/tui/render-helpers.ts +80 -0
  92. package/src/tui/render.ts +1476 -0
@@ -0,0 +1,1028 @@
1
+ /**
2
+ * Core execution logic for running subagents
3
+ */
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync, unlinkSync } from "node:fs";
7
+ import type { Message } from "@earendil-works/pi-ai";
8
+ import type { AgentConfig } from "../../agents/agents.ts";
9
+ import {
10
+ ensureArtifactsDir,
11
+ getArtifactPaths,
12
+ writeArtifact,
13
+ writeMetadata,
14
+ } from "../../shared/artifacts.ts";
15
+ import {
16
+ type AgentProgress,
17
+ type ArtifactPaths,
18
+ type ControlEvent,
19
+ type ModelAttempt,
20
+ type RunSyncOptions,
21
+ type SingleResult,
22
+ type Usage,
23
+ DEFAULT_MAX_OUTPUT,
24
+ INTERCOM_DETACH_REQUEST_EVENT,
25
+ INTERCOM_DETACH_RESPONSE_EVENT,
26
+ truncateOutput,
27
+ getSubagentDepthEnv,
28
+ } from "../../shared/types.ts";
29
+ import {
30
+ DEFAULT_CONTROL_CONFIG,
31
+ applyChildEventToLifecycle,
32
+ buildControlEvent,
33
+ claimControlNotification,
34
+ deriveActivityState,
35
+ shouldNotifyControlEvent,
36
+ shouldSilenceKill,
37
+ } from "../shared/subagent-control.ts";
38
+ import {
39
+ getFinalOutput,
40
+ findLatestSessionFile,
41
+ detectSubagentError,
42
+ extractToolArgsPreview,
43
+ extractTextFromContent,
44
+ } from "../../shared/utils.ts";
45
+ import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
46
+ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
47
+ import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
48
+ import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
49
+ import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
50
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
51
+ import { readStructuredOutput } from "../shared/structured-output.ts";
52
+ import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
53
+ import {
54
+ buildModelCandidates,
55
+ formatModelAttemptNote,
56
+ isRetryableModelFailure,
57
+ } from "../shared/model-fallback.ts";
58
+ import {
59
+ createMutatingFailureState,
60
+ didMutatingToolFail,
61
+ isMutatingTool,
62
+ nextLongRunningTrigger,
63
+ recordMutatingFailure,
64
+ resetMutatingFailureState,
65
+ resolveCurrentPath,
66
+ shouldEscalateMutatingFailures,
67
+ summarizeRecentMutatingFailures,
68
+ } from "../shared/long-running-guard.ts";
69
+ import { acceptanceFailureMessage, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
70
+
71
+ const artifactOutputByResult = new WeakMap<SingleResult, string>();
72
+ const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
73
+
74
+ function emptyUsage(): Usage {
75
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
76
+ }
77
+
78
+ function sumUsage(target: Usage, source: Usage): void {
79
+ target.input += source.input;
80
+ target.output += source.output;
81
+ target.cacheRead += source.cacheRead;
82
+ target.cacheWrite += source.cacheWrite;
83
+ target.cost += source.cost;
84
+ target.turns += source.turns;
85
+ }
86
+
87
+ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
88
+ if (lines.length === 0) return;
89
+ progress.recentOutput.push(...lines.filter((line) => line.trim()));
90
+ if (progress.recentOutput.length > 50) {
91
+ progress.recentOutput.splice(0, progress.recentOutput.length - 50);
92
+ }
93
+ }
94
+
95
+ function stripAcceptanceReportsFromMessages(messages: Message[] | undefined): void {
96
+ for (const message of messages ?? []) {
97
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
98
+ for (const part of message.content) {
99
+ if (part.type === "text" && "text" in part && typeof part.text === "string") {
100
+ part.text = stripAcceptanceReport(part.text);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ function snapshotProgress(progress: AgentProgress): AgentProgress {
107
+ return {
108
+ ...progress,
109
+ skills: progress.skills ? [...progress.skills] : undefined,
110
+ recentTools: progress.recentTools.map((tool) => ({ ...tool })),
111
+ recentOutput: [...progress.recentOutput],
112
+ };
113
+ }
114
+
115
+ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleResult {
116
+ return {
117
+ ...result,
118
+ messages: result.outputMode === "file-only" && result.savedOutputPath ? undefined : result.messages ? [...result.messages] : undefined,
119
+ usage: { ...result.usage },
120
+ skills: result.skills ? [...result.skills] : undefined,
121
+ attemptedModels: result.attemptedModels ? [...result.attemptedModels] : undefined,
122
+ modelAttempts: result.modelAttempts
123
+ ? result.modelAttempts.map((attempt) => ({
124
+ ...attempt,
125
+ usage: attempt.usage ? { ...attempt.usage } : undefined,
126
+ }))
127
+ : undefined,
128
+ controlEvents: result.controlEvents ? result.controlEvents.map((event) => ({ ...event })) : undefined,
129
+ progress,
130
+ progressSummary: result.progressSummary ? { ...result.progressSummary } : undefined,
131
+ artifactPaths: result.artifactPaths ? { ...result.artifactPaths } : undefined,
132
+ truncation: result.truncation ? { ...result.truncation } : undefined,
133
+ outputReference: result.outputReference ? { ...result.outputReference } : undefined,
134
+ };
135
+ }
136
+
137
+ async function runSingleAttempt(
138
+ runtimeCwd: string,
139
+ agent: AgentConfig,
140
+ task: string,
141
+ model: string | undefined,
142
+ options: RunSyncOptions,
143
+ shared: {
144
+ sessionEnabled: boolean;
145
+ systemPrompt: string;
146
+ resolvedSkillNames?: string[];
147
+ skillsWarning?: string;
148
+ jsonlPath?: string;
149
+ artifactPaths?: ArtifactPaths;
150
+ attemptNotes: string[];
151
+ outputSnapshot?: SingleOutputSnapshot;
152
+ originalTask?: string;
153
+ },
154
+ ): Promise<SingleResult> {
155
+ const modelArg = applyThinkingSuffix(model, agent.thinking);
156
+ const { args, env: sharedEnv, tempDir } = buildPiArgs({
157
+ baseArgs: ["--mode", "json", "-p"],
158
+ task,
159
+ sessionEnabled: shared.sessionEnabled,
160
+ sessionDir: options.sessionDir,
161
+ sessionFile: options.sessionFile,
162
+ model,
163
+ thinking: agent.thinking,
164
+ systemPromptMode: agent.systemPromptMode,
165
+ inheritProjectContext: agent.inheritProjectContext,
166
+ inheritSkills: agent.inheritSkills,
167
+ tools: agent.tools,
168
+ extensions: agent.extensions,
169
+ systemPrompt: shared.systemPrompt,
170
+ cwd: options.cwd ?? runtimeCwd,
171
+ promptFileStem: agent.name,
172
+ intercomSessionName: options.intercomSessionName,
173
+ orchestratorIntercomTarget: options.orchestratorIntercomTarget,
174
+ runId: options.runId,
175
+ childAgentName: agent.name,
176
+ childIndex: options.index ?? 0,
177
+ parentEventSink: options.nestedRoute?.eventSink,
178
+ parentControlInbox: options.nestedRoute?.controlInbox,
179
+ parentRootRunId: options.nestedRoute?.rootRunId,
180
+ parentCapabilityToken: options.nestedRoute?.capabilityToken,
181
+ structuredOutput: options.structuredOutput,
182
+ });
183
+
184
+ const result: SingleResult = {
185
+ agent: agent.name,
186
+ task: shared.originalTask ?? task,
187
+ exitCode: 0,
188
+ messages: [],
189
+ usage: emptyUsage(),
190
+ model: modelArg,
191
+ artifactPaths: shared.artifactPaths,
192
+ skills: shared.resolvedSkillNames,
193
+ skillsWarning: shared.skillsWarning,
194
+ };
195
+ const startTime = Date.now();
196
+ if (options.structuredOutput) {
197
+ try {
198
+ if (existsSync(options.structuredOutput.outputPath)) unlinkSync(options.structuredOutput.outputPath);
199
+ } catch {
200
+ // Missing/stale structured-output files are handled after the child exits.
201
+ }
202
+ }
203
+ const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
204
+ let interruptedByControl = false;
205
+ const allControlEvents: ControlEvent[] = [];
206
+ let pendingControlEvents: ControlEvent[] = [];
207
+ const emittedControlEventKeys = new Set<string>();
208
+ const emitControlEvent = (event: ControlEvent) => {
209
+ if (!shouldNotifyControlEvent(controlConfig, event)) return;
210
+ if (!claimControlNotification(controlConfig, event, emittedControlEventKeys)) return;
211
+ allControlEvents.push(event);
212
+ pendingControlEvents.push(event);
213
+ options.onControlEvent?.(event);
214
+ };
215
+
216
+ const progress: AgentProgress = {
217
+ index: options.index ?? 0,
218
+ agent: agent.name,
219
+ status: "running",
220
+ task,
221
+ skills: shared.resolvedSkillNames,
222
+ recentTools: [],
223
+ recentOutput: [...shared.attemptNotes],
224
+ toolCount: 0,
225
+ tokens: 0,
226
+ durationMs: 0,
227
+ lastActivityAt: startTime,
228
+ turnOpen: false,
229
+ lastProductiveSignalAt: startTime,
230
+ };
231
+ result.progress = progress;
232
+ const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
233
+ let observedMutationAttempt = false;
234
+
235
+ const exitCode = await new Promise<number>((resolve) => {
236
+ const spawnSpec = getPiSpawnCommand(args);
237
+ const proc = spawn(spawnSpec.command, spawnSpec.args, {
238
+ cwd: options.cwd ?? runtimeCwd,
239
+ env: spawnEnv,
240
+ stdio: ["ignore", "pipe", "pipe"],
241
+ windowsHide: true,
242
+ });
243
+ const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
244
+ let buf = "";
245
+ let processClosed = false;
246
+ let settled = false;
247
+ let detached = false;
248
+ let intercomStarted = false;
249
+ let assistantError: string | undefined;
250
+ let removeAbortListener: (() => void) | undefined;
251
+ let removeInterruptListener: (() => void) | undefined;
252
+ let activityTimer: NodeJS.Timeout | undefined;
253
+
254
+ const detachForIntercom = () => {
255
+ detached = true;
256
+ processClosed = true;
257
+ result.detached = true;
258
+ result.detachedReason = "intercom coordination";
259
+ progress.status = "detached";
260
+ progress.durationMs = Date.now() - startTime;
261
+ result.progressSummary = {
262
+ toolCount: progress.toolCount,
263
+ tokens: progress.tokens,
264
+ durationMs: progress.durationMs,
265
+ };
266
+ finish(-2);
267
+ };
268
+
269
+ // If the child emits a terminal assistant stop but never exits,
270
+ // give it a short grace period to flush naturally, then clean it up.
271
+ const FINAL_STOP_GRACE_MS = 1000;
272
+ const HARD_KILL_MS = 3000;
273
+ let childExited = false;
274
+ let forcedTerminationSignal = false;
275
+ let cleanTerminalAssistantStopReceived = false;
276
+ let finalDrainTimer: NodeJS.Timeout | undefined;
277
+ let finalHardKillTimer: NodeJS.Timeout | undefined;
278
+ const clearFinalDrainTimers = () => {
279
+ if (finalDrainTimer) {
280
+ clearTimeout(finalDrainTimer);
281
+ finalDrainTimer = undefined;
282
+ }
283
+ if (finalHardKillTimer) {
284
+ clearTimeout(finalHardKillTimer);
285
+ finalHardKillTimer = undefined;
286
+ }
287
+ };
288
+ const startFinalDrain = () => {
289
+ if (childExited || finalDrainTimer || settled || processClosed || detached) return;
290
+ finalDrainTimer = setTimeout(() => {
291
+ if (settled || processClosed || detached) return;
292
+ const termSent = trySignalChild(proc, "SIGTERM");
293
+ if (!termSent) return;
294
+ forcedTerminationSignal = true;
295
+ if (!cleanTerminalAssistantStopReceived && !assistantError) {
296
+ result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
297
+ }
298
+ finalHardKillTimer = setTimeout(() => {
299
+ if (settled || processClosed || detached) return;
300
+ forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
301
+ }, HARD_KILL_MS);
302
+ finalHardKillTimer.unref?.();
303
+ }, FINAL_STOP_GRACE_MS);
304
+ finalDrainTimer.unref?.();
305
+ };
306
+
307
+ let silenceKillRequested = false;
308
+ const requestSilenceKill = (silenceMs: number) => {
309
+ if (silenceKillRequested || childExited || settled || processClosed || detached) return;
310
+ silenceKillRequested = true;
311
+ const termSent = trySignalChild(proc, "SIGTERM");
312
+ if (!termSent) return;
313
+ forcedTerminationSignal = true;
314
+ result.error = result.error ??
315
+ `Subagent killed: in-flight turn produced no output for ${Math.round(silenceMs / 1000)}s ` +
316
+ `(exceeded inFlightSilenceKillMs=${controlConfig.inFlightSilenceKillMs}ms). Likely wedged in a tool call.`;
317
+ const hardKill = setTimeout(() => {
318
+ if (settled || processClosed || detached) return;
319
+ forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
320
+ }, HARD_KILL_MS);
321
+ hardKill.unref?.();
322
+ };
323
+
324
+ const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
325
+ if (!options.allowIntercomDetach || detached || processClosed || !intercomStarted) return;
326
+ if (!payload || typeof payload !== "object") return;
327
+ const requestId = (payload as { requestId?: unknown }).requestId;
328
+ if (typeof requestId !== "string" || requestId.length === 0) return;
329
+ options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true });
330
+ detachForIntercom();
331
+ });
332
+
333
+ const finish = (code: number) => {
334
+ if (settled) return;
335
+ settled = true;
336
+ clearFinalDrainTimers();
337
+ clearStdioGuard();
338
+ if (activityTimer) {
339
+ clearInterval(activityTimer);
340
+ activityTimer = undefined;
341
+ }
342
+ unsubscribeIntercomDetach?.();
343
+ removeAbortListener?.();
344
+ removeInterruptListener?.();
345
+ resolve(code);
346
+ };
347
+
348
+ const drainPendingControlEvents = (): ControlEvent[] | undefined => {
349
+ if (pendingControlEvents.length === 0) return undefined;
350
+ const events = pendingControlEvents;
351
+ pendingControlEvents = [];
352
+ return events;
353
+ };
354
+
355
+ let activeLongRunningNotified = false;
356
+ let pendingToolResult: { tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined;
357
+ const mutatingFailures = createMutatingFailureState();
358
+ const mutatingFailureWindowMs = 5 * 60_000;
359
+ const currentToolDurationMs = (now: number) => progress.currentToolStartedAt ? Math.max(0, now - progress.currentToolStartedAt) : undefined;
360
+ const emitNeedsAttention = (now: number, input: { message?: string; reason?: ControlEvent["reason"]; recentFailureSummary?: string; currentTool?: string; currentPath?: string; currentToolDurationMs?: number } = {}): boolean => {
361
+ if (!controlConfig.enabled) return false;
362
+ const previous = progress.activityState;
363
+ progress.activityState = "needs_attention";
364
+ const event = buildControlEvent({
365
+ type: "needs_attention",
366
+ from: previous,
367
+ to: "needs_attention",
368
+ runId: options.runId,
369
+ agent: agent.name,
370
+ index: options.index,
371
+ ts: now,
372
+ lastActivityAt: progress.lastActivityAt,
373
+ message: input.message,
374
+ reason: input.reason ?? "idle",
375
+ turns: result.usage.turns,
376
+ tokens: progress.tokens,
377
+ toolCount: progress.toolCount,
378
+ currentTool: input.currentTool ?? progress.currentTool,
379
+ currentToolDurationMs: input.currentToolDurationMs ?? currentToolDurationMs(now),
380
+ currentPath: input.currentPath ?? progress.currentPath,
381
+ recentFailureSummary: input.recentFailureSummary,
382
+ });
383
+ emitControlEvent(event);
384
+ return previous !== "needs_attention";
385
+ };
386
+ const emitActiveLongRunning = (now: number, reason: ControlEvent["reason"]): boolean => {
387
+ if (!controlConfig.enabled || activeLongRunningNotified || progress.activityState === "needs_attention") return false;
388
+ activeLongRunningNotified = true;
389
+ const previous = progress.activityState;
390
+ progress.activityState = "active_long_running";
391
+ emitControlEvent(buildControlEvent({
392
+ type: "active_long_running",
393
+ from: previous,
394
+ to: "active_long_running",
395
+ runId: options.runId,
396
+ agent: agent.name,
397
+ index: options.index,
398
+ ts: now,
399
+ message: `${agent.name} is still active but long-running`,
400
+ reason,
401
+ turns: result.usage.turns,
402
+ tokens: progress.tokens,
403
+ toolCount: progress.toolCount,
404
+ currentTool: progress.currentTool,
405
+ currentToolDurationMs: currentToolDurationMs(now),
406
+ currentPath: progress.currentPath,
407
+ elapsedMs: now - startTime,
408
+ }));
409
+ return true;
410
+ };
411
+ const updateActivityState = (now: number): boolean => {
412
+ if (!controlConfig.enabled) return false;
413
+ const idleState = deriveActivityState({
414
+ config: controlConfig,
415
+ startedAt: startTime,
416
+ lastActivityAt: progress.lastActivityAt,
417
+ now,
418
+ inFlightTurn: progress.turnOpen,
419
+ lastProductiveSignalAt: progress.lastProductiveSignalAt,
420
+ });
421
+ if (idleState === "needs_attention") {
422
+ const notified = progress.activityState === "needs_attention" ? false : emitNeedsAttention(now);
423
+ if (shouldSilenceKill({
424
+ turnOpen: progress.turnOpen,
425
+ lastProductiveSignalAt: progress.lastProductiveSignalAt,
426
+ startedAt: startTime,
427
+ now,
428
+ killMs: controlConfig.inFlightSilenceKillMs,
429
+ })) {
430
+ requestSilenceKill(now - (progress.lastProductiveSignalAt ?? startTime));
431
+ }
432
+ return notified;
433
+ }
434
+ const activeReason = nextLongRunningTrigger(controlConfig, {
435
+ startedAt: startTime,
436
+ now,
437
+ turns: result.usage.turns,
438
+ tokens: progress.tokens,
439
+ });
440
+ return activeReason ? emitActiveLongRunning(now, activeReason) : false;
441
+ };
442
+
443
+
444
+ const emitUpdateSnapshot = (text: string) => {
445
+ if (!options.onUpdate || processClosed) return;
446
+ const progressSnapshot = snapshotProgress(progress);
447
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
448
+ const controlEvents = drainPendingControlEvents();
449
+ options.onUpdate({
450
+ content: [{ type: "text", text }],
451
+ details: {
452
+ mode: "single",
453
+ results: [resultSnapshot],
454
+ progress: [progressSnapshot],
455
+ controlEvents,
456
+ },
457
+ });
458
+ };
459
+
460
+ const fireUpdate = () => {
461
+ if (!options.onUpdate || processClosed) return;
462
+ progress.durationMs = Date.now() - startTime;
463
+ emitUpdateSnapshot(getFinalOutput(result.messages) || "(running...)");
464
+ };
465
+
466
+ const processLine = (line: string) => {
467
+ if (!line.trim()) return;
468
+ jsonlWriter.writeLine(line);
469
+ let evt: { type?: string; message?: Message; toolName?: string; args?: unknown };
470
+ try {
471
+ evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
472
+ } catch {
473
+ // Non-JSON stdout lines are expected; only structured events are parsed.
474
+ return;
475
+ }
476
+
477
+ const now = Date.now();
478
+ progress.durationMs = now - startTime;
479
+ progress.lastActivityAt = now;
480
+ const lifecycle = applyChildEventToLifecycle(
481
+ { turnOpen: progress.turnOpen, lastProductiveSignalAt: progress.lastProductiveSignalAt },
482
+ {
483
+ type: evt.type,
484
+ hasToolCall: evt.type === "message_end" && Array.isArray(evt.message?.content)
485
+ && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall"),
486
+ },
487
+ now,
488
+ );
489
+ progress.turnOpen = lifecycle.turnOpen;
490
+ progress.lastProductiveSignalAt = lifecycle.lastProductiveSignalAt;
491
+ updateActivityState(now);
492
+
493
+ if (evt.type === "tool_execution_start") {
494
+ const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
495
+ ? evt.args as Record<string, unknown>
496
+ : {};
497
+ if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
498
+ intercomStarted = true;
499
+ }
500
+ progress.toolCount++;
501
+ progress.currentTool = evt.toolName;
502
+ progress.currentToolArgs = extractToolArgsPreview(toolArgs);
503
+ progress.currentToolStartedAt = now;
504
+ progress.currentPath = resolveCurrentPath(evt.toolName, toolArgs);
505
+ const mutates = isMutatingTool(evt.toolName, toolArgs);
506
+ observedMutationAttempt = observedMutationAttempt || mutates;
507
+ pendingToolResult = { tool: evt.toolName ?? "tool", path: progress.currentPath, mutates, startedAt: now };
508
+ fireUpdate();
509
+ }
510
+
511
+ if (evt.type === "tool_execution_end") {
512
+ if (progress.currentTool) {
513
+ progress.recentTools.push({
514
+ tool: progress.currentTool,
515
+ args: progress.currentToolArgs || "",
516
+ endMs: now,
517
+ });
518
+ }
519
+ progress.currentTool = undefined;
520
+ progress.currentToolArgs = undefined;
521
+ progress.currentToolStartedAt = undefined;
522
+ progress.currentPath = undefined;
523
+ fireUpdate();
524
+ }
525
+
526
+ if (evt.type === "message_end" && evt.message) {
527
+ result.messages.push(evt.message);
528
+ if (evt.message.role === "assistant") {
529
+ result.usage.turns++;
530
+ progress.turnCount = result.usage.turns;
531
+ const u = evt.message.usage;
532
+ if (u) {
533
+ result.usage.input += u.input || 0;
534
+ result.usage.output += u.output || 0;
535
+ result.usage.cacheRead += u.cacheRead || 0;
536
+ result.usage.cacheWrite += u.cacheWrite || 0;
537
+ result.usage.cost += u.cost?.total || 0;
538
+ progress.tokens = result.usage.input + result.usage.output;
539
+ }
540
+ if (!result.model && evt.message.model) result.model = evt.message.model;
541
+ if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
542
+ const assistantText = extractTextFromContent(evt.message.content);
543
+ appendRecentOutput(progress, assistantText.split("\n").slice(-10));
544
+ // Final assistant message: start the exit drain window.
545
+ const stopReason = (evt.message as { stopReason?: string }).stopReason;
546
+ const hasToolCall = Array.isArray(evt.message.content)
547
+ && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
548
+ if (stopReason === "stop" && !hasToolCall) {
549
+ if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
550
+ cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
551
+ startFinalDrain();
552
+ }
553
+ }
554
+ updateActivityState(now);
555
+ fireUpdate();
556
+ }
557
+
558
+ if (evt.type === "tool_result_end" && evt.message) {
559
+ result.messages.push(evt.message);
560
+ const resultText = extractTextFromContent(evt.message.content);
561
+ appendRecentOutput(progress, resultText.split("\n").slice(-10));
562
+ const toolSnapshot = pendingToolResult;
563
+ pendingToolResult = undefined;
564
+ if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
565
+ recordMutatingFailure(mutatingFailures, {
566
+ tool: toolSnapshot.tool,
567
+ path: toolSnapshot.path,
568
+ error: resultText.split("\n").find((line) => line.trim())?.trim().slice(0, 180) ?? "mutating tool failed",
569
+ ts: now,
570
+ }, mutatingFailureWindowMs);
571
+ if (shouldEscalateMutatingFailures(mutatingFailures, controlConfig.failedToolAttemptsBeforeAttention)) {
572
+ emitNeedsAttention(now, {
573
+ message: `${agent.name} needs attention after repeated mutating tool failures`,
574
+ reason: "tool_failures",
575
+ currentTool: toolSnapshot.tool,
576
+ currentPath: toolSnapshot.path,
577
+ currentToolDurationMs: toolSnapshot.startedAt ? Math.max(0, now - toolSnapshot.startedAt) : undefined,
578
+ recentFailureSummary: summarizeRecentMutatingFailures(mutatingFailures),
579
+ });
580
+ }
581
+ } else if (toolSnapshot?.mutates) {
582
+ resetMutatingFailureState(mutatingFailures);
583
+ }
584
+ fireUpdate();
585
+ }
586
+ };
587
+
588
+ if (controlConfig.enabled) {
589
+ activityTimer = setInterval(() => {
590
+ if (processClosed || settled || detached) return;
591
+ const now = Date.now();
592
+ if (updateActivityState(now)) {
593
+ progress.durationMs = now - startTime;
594
+ fireUpdate();
595
+ }
596
+ }, 1000);
597
+ activityTimer.unref?.();
598
+ }
599
+
600
+ let stderrBuf = "";
601
+
602
+ const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
603
+ proc.stdout.on("data", (d) => {
604
+ buf += d.toString();
605
+ const lines = buf.split("\n");
606
+ buf = lines.pop() || "";
607
+ lines.forEach(processLine);
608
+ });
609
+ proc.stderr.on("data", (d) => {
610
+ stderrBuf += d.toString();
611
+ });
612
+ proc.on("exit", () => {
613
+ childExited = true;
614
+ clearFinalDrainTimers();
615
+ });
616
+ proc.on("close", (code, signal) => {
617
+ clearFinalDrainTimers();
618
+ clearStdioGuard();
619
+ void jsonlWriter.close().catch(() => {
620
+ // JSONL artifact flush is best effort.
621
+ });
622
+ cleanupTempDir(tempDir);
623
+ if (detached) {
624
+ finish(-2);
625
+ return;
626
+ }
627
+ processClosed = true;
628
+ if (buf.trim()) processLine(buf);
629
+ if (!result.error && assistantError) result.error = assistantError;
630
+ const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !result.error;
631
+ if (code !== 0 && stderrBuf.trim() && !result.error && !forcedDrainAfterFinalSuccess) {
632
+ result.error = stderrBuf.trim();
633
+ }
634
+ const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
635
+ finish(finalCode);
636
+ });
637
+ proc.on("error", (error) => {
638
+ clearFinalDrainTimers();
639
+ clearStdioGuard();
640
+ void jsonlWriter.close().catch(() => {
641
+ // JSONL artifact flush is best effort.
642
+ });
643
+ cleanupTempDir(tempDir);
644
+ if (!result.error) {
645
+ result.error = error instanceof Error ? error.message : String(error);
646
+ }
647
+ finish(1);
648
+ });
649
+
650
+ if (options.signal) {
651
+ const kill = () => {
652
+ if (processClosed || detached) return;
653
+ if (options.allowIntercomDetach && intercomStarted && !detached) {
654
+ detachForIntercom();
655
+ return;
656
+ }
657
+ proc.kill("SIGTERM");
658
+ setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
659
+ };
660
+ if (options.signal.aborted) kill();
661
+ else {
662
+ options.signal.addEventListener("abort", kill, { once: true });
663
+ removeAbortListener = () => options.signal?.removeEventListener("abort", kill);
664
+ }
665
+ }
666
+
667
+ if (options.interruptSignal) {
668
+ const interrupt = () => {
669
+ if (processClosed || detached || settled) return;
670
+ interruptedByControl = true;
671
+ progress.status = "running";
672
+ progress.durationMs = Date.now() - startTime;
673
+ result.interrupted = true;
674
+ result.finalOutput = "Interrupted. Waiting for explicit next action.";
675
+ progress.activityState = undefined;
676
+ fireUpdate();
677
+ trySignalChild(proc, "SIGINT");
678
+ setTimeout(() => {
679
+ if (settled || processClosed || detached) return;
680
+ trySignalChild(proc, "SIGTERM");
681
+ }, 1000).unref?.();
682
+ };
683
+ if (options.interruptSignal.aborted) interrupt();
684
+ else {
685
+ options.interruptSignal.addEventListener("abort", interrupt, { once: true });
686
+ removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
687
+ }
688
+ }
689
+ });
690
+ result.exitCode = exitCode;
691
+ if (interruptedByControl) {
692
+ result.exitCode = 0;
693
+ result.interrupted = true;
694
+ result.error = undefined;
695
+ result.finalOutput = result.finalOutput || "Interrupted. Waiting for explicit next action.";
696
+ result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
697
+ progress.activityState = undefined;
698
+ progress.durationMs = Date.now() - startTime;
699
+ result.progressSummary = {
700
+ toolCount: progress.toolCount,
701
+ tokens: progress.tokens,
702
+ durationMs: progress.durationMs,
703
+ };
704
+ return result;
705
+ }
706
+ if (result.detached) {
707
+ result.exitCode = 0;
708
+ result.finalOutput = "Detached for intercom coordination.";
709
+ return result;
710
+ }
711
+
712
+ if (result.error && result.exitCode === 0) {
713
+ result.exitCode = 1;
714
+ }
715
+ if (result.exitCode === 0 && !result.error) {
716
+ const errInfo = detectSubagentError(result.messages);
717
+ if (errInfo.hasError) {
718
+ result.exitCode = errInfo.exitCode ?? 1;
719
+ result.error = errInfo.details
720
+ ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
721
+ : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
722
+ }
723
+ }
724
+ if (options.structuredOutput && result.exitCode === 0 && !result.error) {
725
+ const structured = readStructuredOutput({
726
+ schema: options.structuredOutput.schema,
727
+ schemaPath: options.structuredOutput.schemaPath,
728
+ outputPath: options.structuredOutput.outputPath,
729
+ });
730
+ result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
731
+ result.structuredOutputPath = options.structuredOutput.outputPath;
732
+ if (structured.error) {
733
+ result.exitCode = 1;
734
+ result.error = structured.error;
735
+ } else {
736
+ result.structuredOutput = structured.value;
737
+ }
738
+ }
739
+
740
+ progress.status = result.exitCode === 0 ? "completed" : "failed";
741
+ progress.durationMs = Date.now() - startTime;
742
+ if (result.error) {
743
+ progress.error = result.error;
744
+ if (progress.currentTool) {
745
+ progress.failedTool = progress.currentTool;
746
+ }
747
+ }
748
+
749
+ result.progressSummary = {
750
+ toolCount: progress.toolCount,
751
+ tokens: progress.tokens,
752
+ durationMs: progress.durationMs,
753
+ };
754
+
755
+ const acceptanceOutput = getFinalOutput(result.messages);
756
+ let fullOutput = stripAcceptanceReport(acceptanceOutput);
757
+ const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
758
+ ? evaluateCompletionMutationGuard({
759
+ agent: agent.name,
760
+ task: shared.originalTask ?? task,
761
+ messages: result.messages,
762
+ tools: agent.tools,
763
+ })
764
+ : undefined;
765
+ if (completionGuard?.triggered && !observedMutationAttempt) {
766
+ result.exitCode = 1;
767
+ result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
768
+ progress.status = "failed";
769
+ progress.error = result.error;
770
+ emitControlEvent(buildControlEvent({
771
+ from: progress.activityState,
772
+ to: "needs_attention",
773
+ runId: options.runId ?? agent.name,
774
+ agent: agent.name,
775
+ index: options.index,
776
+ ts: Date.now(),
777
+ message: `${agent.name} completed without making edits for an implementation task`,
778
+ reason: "completion_guard",
779
+ }));
780
+ }
781
+ if (options.outputPath && result.exitCode === 0) {
782
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
783
+ fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
784
+ result.savedOutputPath = resolvedOutput.savedPath;
785
+ result.outputSaveError = resolvedOutput.saveError;
786
+ if (resolvedOutput.savedPath) {
787
+ result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
788
+ }
789
+ }
790
+ artifactOutputByResult.set(result, fullOutput);
791
+ acceptanceOutputByResult.set(result, acceptanceOutput);
792
+ result.outputMode = options.outputMode ?? "inline";
793
+ result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
794
+ ? result.outputReference.message
795
+ : fullOutput;
796
+ result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
797
+ if (options.onUpdate) {
798
+ const finalText = result.finalOutput || result.error || "(no output)";
799
+ const progressSnapshot = snapshotProgress(progress);
800
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
801
+ options.onUpdate({
802
+ content: [{ type: "text", text: finalText }],
803
+ details: {
804
+ mode: "single",
805
+ results: [resultSnapshot],
806
+ progress: [progressSnapshot],
807
+ controlEvents: allControlEvents.length ? allControlEvents : undefined,
808
+ },
809
+ });
810
+ }
811
+ return result;
812
+ }
813
+
814
+ /**
815
+ * Run a subagent synchronously (blocking until complete)
816
+ */
817
+ export async function runSync(
818
+ runtimeCwd: string,
819
+ agents: AgentConfig[],
820
+ agentName: string,
821
+ task: string,
822
+ options: RunSyncOptions,
823
+ ): Promise<SingleResult> {
824
+ const agent = agents.find((a) => a.name === agentName);
825
+ if (!agent) {
826
+ return {
827
+ agent: agentName,
828
+ task,
829
+ exitCode: 1,
830
+ messages: [],
831
+ usage: emptyUsage(),
832
+ error: `Unknown agent: ${agentName}`,
833
+ };
834
+ }
835
+ const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
836
+ if (outputModeValidationError) {
837
+ return {
838
+ agent: agentName,
839
+ task,
840
+ exitCode: 1,
841
+ messages: [],
842
+ usage: emptyUsage(),
843
+ outputMode: options.outputMode,
844
+ error: outputModeValidationError,
845
+ };
846
+ }
847
+
848
+ const shareEnabled = options.share === true;
849
+ const effectiveAcceptance = resolveEffectiveAcceptance({
850
+ explicit: options.acceptance,
851
+ agentName,
852
+ task,
853
+ mode: options.acceptanceContext?.mode ?? "single",
854
+ async: options.acceptanceContext?.async,
855
+ dynamic: options.acceptanceContext?.dynamic,
856
+ dynamicGroup: options.acceptanceContext?.dynamicGroup,
857
+ });
858
+ const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
859
+ const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
860
+ const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
861
+ const skillNames = options.skills ?? agent.skills ?? [];
862
+ const skillCwd = options.cwd ?? runtimeCwd;
863
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
864
+ if (skillNames.some((skill) => skill.trim() === "pi-cohort") && missingSkills.includes("pi-cohort")) {
865
+ return {
866
+ agent: agentName,
867
+ task,
868
+ exitCode: 1,
869
+ messages: [],
870
+ usage: emptyUsage(),
871
+ error: "Skills not found: pi-cohort",
872
+ };
873
+ }
874
+ let systemPrompt = agent.systemPrompt?.trim() || "";
875
+ if (resolvedSkills.length > 0) {
876
+ const skillInjection = buildSkillInjection(resolvedSkills);
877
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
878
+ }
879
+
880
+ const candidates = buildModelCandidates(
881
+ options.modelOverride ?? agent.model,
882
+ agent.fallbackModels,
883
+ options.availableModels,
884
+ options.preferredModelProvider,
885
+ );
886
+ const attemptedModels: string[] = [];
887
+ const modelAttempts: ModelAttempt[] = [];
888
+ const aggregateUsage = emptyUsage();
889
+ const attemptNotes: string[] = [];
890
+ let totalToolCount = 0;
891
+ let totalDurationMs = 0;
892
+
893
+ let artifactPathsResult: ArtifactPaths | undefined;
894
+ let jsonlPath: string | undefined;
895
+ if (options.artifactsDir && options.artifactConfig?.enabled !== false) {
896
+ artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
897
+ ensureArtifactsDir(options.artifactsDir);
898
+ if (options.artifactConfig?.includeInput !== false) {
899
+ writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${taskWithAcceptance}`);
900
+ }
901
+ if (options.artifactConfig?.includeJsonl !== false) {
902
+ jsonlPath = artifactPathsResult.jsonlPath;
903
+ }
904
+ }
905
+
906
+ let lastResult: SingleResult | undefined;
907
+ const modelsToTry = candidates.length > 0 ? candidates : [undefined];
908
+ for (let i = 0; i < modelsToTry.length; i++) {
909
+ const candidate = modelsToTry[i];
910
+ if (candidate) attemptedModels.push(candidate);
911
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
912
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
913
+ sessionEnabled,
914
+ systemPrompt,
915
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
916
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
917
+ jsonlPath,
918
+ artifactPaths: artifactPathsResult,
919
+ attemptNotes,
920
+ outputSnapshot,
921
+ originalTask: task,
922
+ });
923
+ lastResult = result;
924
+ sumUsage(aggregateUsage, result.usage);
925
+ totalToolCount += result.progressSummary?.toolCount ?? 0;
926
+ totalDurationMs += result.progressSummary?.durationMs ?? 0;
927
+ const attemptSucceeded = result.exitCode === 0 && !result.error;
928
+ const attempt: ModelAttempt = {
929
+ model: candidate ?? result.model ?? agent.model ?? "default",
930
+ success: attemptSucceeded,
931
+ exitCode: result.exitCode,
932
+ error: result.error,
933
+ usage: { ...result.usage },
934
+ };
935
+ modelAttempts.push(attempt);
936
+ if (attemptSucceeded) {
937
+ break;
938
+ }
939
+ if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
940
+ break;
941
+ }
942
+ attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
943
+ }
944
+
945
+ const result = lastResult ?? {
946
+ agent: agentName,
947
+ task,
948
+ exitCode: 1,
949
+ messages: [],
950
+ usage: emptyUsage(),
951
+ error: "Subagent did not produce a result.",
952
+ } satisfies SingleResult;
953
+
954
+ result.usage = aggregateUsage;
955
+ result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
956
+ result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
957
+ result.progressSummary = {
958
+ toolCount: totalToolCount,
959
+ tokens: aggregateUsage.input + aggregateUsage.output,
960
+ durationMs: totalDurationMs,
961
+ };
962
+ if (attemptNotes.length > 0 && result.progress) {
963
+ result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
964
+ if (result.progress.recentOutput.length > 50) {
965
+ result.progress.recentOutput.splice(50);
966
+ }
967
+ }
968
+
969
+ if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
970
+ result.artifactPaths = artifactPathsResult;
971
+ if (options.artifactConfig?.includeOutput !== false) {
972
+ writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
973
+ }
974
+ if (options.artifactConfig?.includeMetadata !== false) {
975
+ writeMetadata(artifactPathsResult.metadataPath, {
976
+ runId: options.runId,
977
+ agent: agentName,
978
+ task,
979
+ exitCode: result.exitCode,
980
+ usage: result.usage,
981
+ model: result.model,
982
+ attemptedModels: result.attemptedModels,
983
+ modelAttempts: result.modelAttempts,
984
+ durationMs: result.progressSummary?.durationMs,
985
+ toolCount: result.progressSummary?.toolCount,
986
+ error: result.error,
987
+ skills: result.skills,
988
+ skillsWarning: result.skillsWarning,
989
+ timestamp: Date.now(),
990
+ });
991
+ }
992
+
993
+ if (options.maxOutput) {
994
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
995
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
996
+ if (truncationResult.truncated) result.truncation = truncationResult;
997
+ }
998
+ } else if (options.maxOutput) {
999
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1000
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config);
1001
+ if (truncationResult.truncated) result.truncation = truncationResult;
1002
+ }
1003
+
1004
+ if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
1005
+ result.sessionFile = options.sessionFile;
1006
+ } else if (shareEnabled && options.sessionDir) {
1007
+ const sessionFile = findLatestSessionFile(options.sessionDir);
1008
+ if (sessionFile) result.sessionFile = sessionFile;
1009
+ }
1010
+
1011
+ result.acceptance = await evaluateAcceptance({
1012
+ acceptance: effectiveAcceptance,
1013
+ output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
1014
+ cwd: options.cwd ?? runtimeCwd,
1015
+ });
1016
+ const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1017
+ stripAcceptanceReportsFromMessages(result.messages);
1018
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted) {
1019
+ result.exitCode = 1;
1020
+ result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1021
+ if (result.progress) {
1022
+ result.progress.status = "failed";
1023
+ result.progress.error = result.error;
1024
+ }
1025
+ }
1026
+
1027
+ return result;
1028
+ }