@selesai/code 0.1.4 → 0.1.6

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