@gajae-code/coding-agent 0.2.5 → 0.3.1

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 (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
package/src/task/index.ts CHANGED
@@ -31,6 +31,7 @@ import { formatBytes, formatDuration } from "../tools/render-utils";
31
31
  import {
32
32
  type AgentDefinition,
33
33
  type AgentProgress,
34
+ type ForkContextMode,
34
35
  type ForkContextPolicy,
35
36
  getTaskSchema,
36
37
  type SingleResult,
@@ -46,10 +47,13 @@ import { generateCommitMessage } from "../utils/commit-message-generator";
46
47
  import * as git from "../utils/git";
47
48
  import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
48
49
  import { runSubprocess } from "./executor";
50
+ import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
49
51
  import { AgentOutputManager } from "./output-manager";
50
52
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
53
+ import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./receipt";
51
54
  import { renderResult, renderCall as renderTaskCall } from "./render";
52
55
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
56
+ import { DEFAULT_SPAWN_THRESHOLD, evaluateReviewerExploreGate, evaluateSpawnGate } from "./spawn-gate";
53
57
  import {
54
58
  applyNestedPatches,
55
59
  captureBaseline,
@@ -65,6 +69,18 @@ import {
65
69
  type WorktreeBaseline,
66
70
  } from "./worktree";
67
71
 
72
+ interface TaskResumeDescriptor {
73
+ toolCallId: string;
74
+ params: TaskParams;
75
+ task: TaskItem & { id: string };
76
+ sessionFile: string | null;
77
+ forkContextSeed?: ForkContextSeed;
78
+ agentSource: AgentDefinition["source"];
79
+ }
80
+
81
+ function isTaskResumeDescriptor(value: unknown): value is TaskResumeDescriptor {
82
+ return typeof value === "object" && value !== null && "task" in value && "params" in value;
83
+ }
68
84
  function renderSubagentUserPrompt(assignment: string, simpleMode: TaskSimpleMode): string {
69
85
  return prompt.render(subagentUserPromptTemplate, {
70
86
  assignment: assignment.trim(),
@@ -110,11 +126,37 @@ function addUsageTotals(target: Usage, usage: Partial<Usage>): void {
110
126
  target.cost.total += cost.total;
111
127
  }
112
128
 
129
+ function validateTaskIdsForScheduling(tasks: readonly TaskItem[]): string | undefined {
130
+ const invalid: string[] = [];
131
+ for (let i = 0; i < tasks.length; i++) {
132
+ const error = getTaskIdValidationError(tasks[i]?.id);
133
+ if (error) invalid.push(`index ${i}: ${error}`);
134
+ }
135
+ return invalid.length > 0 ? `Invalid task ids: ${invalid.join(" ")}` : undefined;
136
+ }
137
+
113
138
  // Re-export types and utilities
114
139
  export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
115
140
  export { discoverCommands, expandCommand, getCommand } from "./commands";
116
141
  export { discoverAgents, getAgent } from "./discovery";
142
+ export {
143
+ isValidAllocatedTaskId,
144
+ isValidTaskId,
145
+ TASK_ID_DESCRIPTION,
146
+ TASK_ID_PATTERN,
147
+ validateAllocatedTaskId,
148
+ validateTaskId,
149
+ } from "./id";
117
150
  export { AgentOutputManager } from "./output-manager";
151
+ export type { TaskResultReceipt } from "./receipt";
152
+ export {
153
+ assertNoRawTaskFields,
154
+ buildTaskReceipt,
155
+ buildTaskRoi,
156
+ buildTaskRoiSummary,
157
+ findRawTaskLeakKeys,
158
+ sanitizeTaskToolDetails,
159
+ } from "./receipt";
118
160
  export type {
119
161
  AgentDefinition,
120
162
  AgentProgress,
@@ -191,6 +233,14 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
191
233
  if (!customSchemaEnabled && params.schema !== undefined) {
192
234
  disallowedFields.push("schema");
193
235
  }
236
+ if (!contextEnabled) {
237
+ const inheritedTaskIds = (params.tasks ?? [])
238
+ .filter(task => task.inheritContext !== undefined && task.inheritContext !== "none")
239
+ .map(task => task.id);
240
+ if (inheritedTaskIds.length > 0) {
241
+ disallowedFields.push(`inheritContext for task(s) ${inheritedTaskIds.join(", ")}`);
242
+ }
243
+ }
194
244
  if (disallowedFields.length === 0) {
195
245
  return undefined;
196
246
  }
@@ -200,22 +250,77 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
200
250
  }
201
251
 
202
252
  if (disallowedFields.length === 1) {
203
- return `task.simple is set to independent, so the task tool does not accept \`${disallowedFields[0]}\`. Put everything the subagent needs inside each task assignment.`;
253
+ return `task.simple is set to independent, so the task tool does not accept ${disallowedFields[0].startsWith("inheritContext") ? disallowedFields[0] : `\`${disallowedFields[0]}\``}. Put everything the subagent needs inside each task assignment.`;
204
254
  }
205
255
 
206
- return "task.simple is set to independent, so the task tool does not accept `context` or `schema`. Put all required background and output expectations inside each task assignment or the selected agent definition.";
256
+ return `task.simple is set to independent, so the task tool does not accept ${disallowedFields.map(field => (field.startsWith("inheritContext") ? field : `\`${field}\``)).join(", ")}. Put all required background and output expectations inside each task assignment or the selected agent definition.`;
207
257
  }
208
258
 
209
259
  function getForkContextPolicy(agent: AgentDefinition): ForkContextPolicy {
210
260
  return agent.forkContext ?? "forbidden";
211
261
  }
262
+ const FORK_CONTEXT_MODES = [
263
+ "none",
264
+ "receipt",
265
+ "last-turn",
266
+ "bounded",
267
+ "full",
268
+ ] as const satisfies readonly ForkContextMode[];
269
+ const FORK_CONTEXT_MODE_SET = new Set<unknown>(FORK_CONTEXT_MODES);
270
+ const FORK_CONTEXT_REQUEST_MODES = ["receipt", "last-turn", "bounded", "full"] as const satisfies readonly Exclude<
271
+ ForkContextMode,
272
+ "none"
273
+ >[];
274
+ const FORK_CONTEXT_REQUEST_MODE_SET = new Set<unknown>(FORK_CONTEXT_REQUEST_MODES);
275
+
276
+ function isValidForkContextMode(value: unknown): value is ForkContextMode {
277
+ return FORK_CONTEXT_MODE_SET.has(value);
278
+ }
279
+
280
+ function requestsForkContext(
281
+ task: Pick<TaskItem, "inheritContext">,
282
+ ): task is TaskItem & { inheritContext: Exclude<ForkContextMode, "none"> } {
283
+ return FORK_CONTEXT_REQUEST_MODE_SET.has(task.inheritContext);
284
+ }
285
+
286
+ function resolveForkSeedParamsForMode(
287
+ mode: ForkContextMode,
288
+ configuredMaxMessages: number | undefined,
289
+ configuredMaxTokens: number,
290
+ model: Model | undefined,
291
+ ): { maxMessages: number; maxTokens: number } | undefined {
292
+ const capMessages = (defaultMaxMessages: number): number =>
293
+ configuredMaxMessages === undefined
294
+ ? defaultMaxMessages
295
+ : Math.min(defaultMaxMessages, Math.max(0, Math.trunc(configuredMaxMessages)));
296
+ switch (mode) {
297
+ case "none":
298
+ return undefined;
299
+ case "receipt":
300
+ return { maxMessages: 1, maxTokens: 64 };
301
+ case "last-turn":
302
+ return { maxMessages: 2, maxTokens: 250 };
303
+ case "bounded":
304
+ return { maxMessages: capMessages(50), maxTokens: 250 };
305
+ case "full":
306
+ return { maxMessages: capMessages(500), maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, model) };
307
+ default:
308
+ return undefined;
309
+ }
310
+ }
212
311
 
213
312
  function validateForkContextRequests(
214
313
  tasks: readonly TaskItem[],
215
314
  agent: AgentDefinition,
216
315
  forkContextEnabled: boolean,
217
316
  ): string | undefined {
218
- const requested = tasks.filter(task => task.inheritContext === true);
317
+ const invalidTaskIds = tasks
318
+ .filter(task => task.inheritContext !== undefined && !isValidForkContextMode(task.inheritContext as unknown))
319
+ .map(task => task.id);
320
+ if (invalidTaskIds.length > 0) {
321
+ return `Invalid inheritContext for task(s) ${invalidTaskIds.join(", ")}. Allowed modes: ${FORK_CONTEXT_MODES.join(", ")}.`;
322
+ }
323
+ const requested = tasks.filter(requestsForkContext);
219
324
  if (requested.length === 0) return undefined;
220
325
  const taskIds = requested.map(task => task.id).join(", ");
221
326
  if (!forkContextEnabled) {
@@ -227,10 +332,10 @@ function validateForkContextRequests(
227
332
  return undefined;
228
333
  }
229
334
 
230
- function resolveForkContextMaxTokens(configured: number, model: Model | undefined): number {
335
+ export function resolveForkContextMaxTokens(configured: number, model: Model | undefined): number {
231
336
  if (configured > 0) return Math.trunc(configured);
232
337
  const contextWindow = model?.contextWindow ?? 0;
233
- return contextWindow > 0 ? Math.max(1, Math.floor(contextWindow * 0.25)) : 25_000;
338
+ return contextWindow > 0 ? Math.max(1, Math.floor(contextWindow * 0.15)) : 15_000;
234
339
  }
235
340
 
236
341
  // ═══════════════════════════════════════════════════════════════════════════
@@ -252,6 +357,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
252
357
  readonly renderResult = renderResult;
253
358
  readonly #discoveredAgents: AgentDefinition[];
254
359
  readonly #blockedAgent: string | undefined;
360
+ readonly #spawningAgentType: string | undefined;
255
361
 
256
362
  get parameters(): TaskToolSchemaInstance {
257
363
  const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
@@ -283,6 +389,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
283
389
  discoveredAgents: AgentDefinition[],
284
390
  ) {
285
391
  this.#blockedAgent = $env.PI_BLOCKED_AGENT;
392
+ this.#spawningAgentType = session.currentAgentType;
286
393
  this.#discoveredAgents = discoveredAgents;
287
394
  }
288
395
 
@@ -312,10 +419,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
312
419
  }
313
420
 
314
421
  const taskItems = params.tasks ?? [];
422
+ const taskIdValidationError = validateTaskIdsForScheduling(taskItems);
423
+ if (taskIdValidationError) {
424
+ return createTaskModeError(taskIdValidationError);
425
+ }
315
426
  if (taskItems.length === 0) {
316
427
  return this.#executeSync(_toolCallId, params, signal, onUpdate);
317
428
  }
318
-
319
429
  const agent = getAgent(this.#discoveredAgents, params.agent);
320
430
  if (!agent) {
321
431
  const available =
@@ -353,6 +463,36 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
353
463
  return createTaskModeError(forkContextValidationError);
354
464
  }
355
465
 
466
+ const batchGateDecision = evaluateSpawnGate({ childCount: taskItems.length, plan: params.spawnPlan });
467
+ if (batchGateDecision.outcome === "rejected") {
468
+ return {
469
+ content: [
470
+ {
471
+ type: "text",
472
+ text: `Task spawn gate rejected this batch: ${batchGateDecision.reason}. Batches with more than ${DEFAULT_SPAWN_THRESHOLD} tasks require spawnPlan fields: ${batchGateDecision.missingFields.join(", ")}.`,
473
+ },
474
+ ],
475
+ details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
476
+ };
477
+ }
478
+
479
+ const reviewerExploreDecision = evaluateReviewerExploreGate({
480
+ spawningAgentType: this.#spawningAgentType,
481
+ targetAgent: params.agent,
482
+ plan: params.spawnPlan,
483
+ });
484
+ if (reviewerExploreDecision.outcome === "rejected") {
485
+ return {
486
+ content: [
487
+ {
488
+ type: "text",
489
+ text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
490
+ },
491
+ ],
492
+ details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
493
+ };
494
+ }
495
+
356
496
  const manager = AsyncJobManager.instance();
357
497
  if (!manager) {
358
498
  return {
@@ -416,21 +556,75 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
416
556
  };
417
557
 
418
558
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
559
+ if (typeof manager.setResumeRunner === "function") {
560
+ manager.setResumeRunner((_subagentId, message, resumeDescriptor) => {
561
+ const descriptor = isTaskResumeDescriptor(resumeDescriptor?.data) ? resumeDescriptor.data : undefined;
562
+ if (!descriptor) return undefined;
563
+ const forkSeeds = descriptor.forkContextSeed
564
+ ? new Map([[descriptor.task.id, descriptor.forkContextSeed]])
565
+ : undefined;
566
+ return manager.register(
567
+ "task",
568
+ descriptor.task.id,
569
+ async ({ signal: runSignal }) => {
570
+ const result = await this.#executeSync(
571
+ descriptor.toolCallId,
572
+ { ...descriptor.params, tasks: [descriptor.task] },
573
+ runSignal,
574
+ undefined,
575
+ [descriptor.task.id],
576
+ forkSeeds,
577
+ {
578
+ runMode: message ? "message" : "resume",
579
+ resumeMessage: message,
580
+ sessionFiles: new Map([[descriptor.task.id, descriptor.sessionFile]]),
581
+ },
582
+ );
583
+ const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
584
+ const singleResult = result.details?.results[0];
585
+ return singleResult?.paused ? { kind: "paused" } : finalText;
586
+ },
587
+ {
588
+ id: `${descriptor.task.id}-resume-${Snowflake.next()}`,
589
+ ownerId: this.session.getAgentId?.() ?? undefined,
590
+ metadata: {
591
+ subagent: {
592
+ id: descriptor.task.id,
593
+ agent: descriptor.params.agent,
594
+ agentSource: descriptor.agentSource,
595
+ description: descriptor.task.description,
596
+ assignment: descriptor.task.assignment.trim(),
597
+ },
598
+ },
599
+ },
600
+ );
601
+ });
602
+ }
419
603
  const semaphore = new Semaphore(maxConcurrency);
420
604
  const buildForkContextSeedForTask = async (task: TaskItem): Promise<ForkContextSeed | undefined> => {
421
- if (task.inheritContext !== true) return undefined;
605
+ if (!requestsForkContext(task)) return undefined;
422
606
  if (!this.session.buildForkContextSeed) {
423
607
  throw new Error("Current session cannot build fork-context seeds.");
424
608
  }
425
- const maxMessages = this.session.settings.get("task.forkContext.maxMessages");
609
+ const configuredMaxMessages = this.session.settings.has("task.forkContext.maxMessages")
610
+ ? this.session.settings.get("task.forkContext.maxMessages")
611
+ : undefined;
426
612
  const configuredMaxTokens = this.session.settings.get("task.forkContext.maxTokens");
613
+ const params = resolveForkSeedParamsForMode(
614
+ task.inheritContext,
615
+ configuredMaxMessages,
616
+ configuredMaxTokens,
617
+ this.session.model,
618
+ );
619
+ if (!params) return undefined;
427
620
  return await this.session.buildForkContextSeed({
428
- maxMessages,
429
- maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, this.session.model),
621
+ ...params,
430
622
  signal,
431
623
  });
432
624
  };
433
625
  const frozenForkSeeds = new Map<string, ForkContextSeed>();
626
+ const parentSessionFileForBatch = this.session.getSessionFile();
627
+ const batchArtifactsDir = parentSessionFileForBatch ? parentSessionFileForBatch.slice(0, -6) : null;
434
628
 
435
629
  for (let i = 0; i < taskItems.length; i++) {
436
630
  const taskItem = taskItems[i];
@@ -443,12 +637,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
443
637
  continue;
444
638
  }
445
639
 
446
- const uniqueId = uniqueIds[i];
640
+ const uniqueId = validateAllocatedTaskId(uniqueIds[i] ?? "");
447
641
  const frozenForkSeed = await buildForkContextSeedForTask(taskItem);
448
642
  if (frozenForkSeed) frozenForkSeeds.set(uniqueId, frozenForkSeed);
449
643
  const singleParams: TaskParams = { ...params, tasks: [taskItem] };
450
644
  const label = uniqueId;
451
645
  try {
646
+ const subtaskSessionFile = batchArtifactsDir ? path.join(batchArtifactsDir, `${uniqueId}.jsonl`) : null;
452
647
  const jobId = manager.register(
453
648
  "task",
454
649
  label,
@@ -478,22 +673,32 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
478
673
  undefined,
479
674
  [uniqueId],
480
675
  frozenForkSeeds,
676
+ {
677
+ sessionFiles: new Map([[uniqueId, subtaskSessionFile]]),
678
+ },
481
679
  );
482
680
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
483
681
  const singleResult = result.details?.results[0];
484
682
  if (progress) {
485
- progress.status = singleResult?.aborted
486
- ? "aborted"
487
- : (singleResult?.exitCode ?? 0) === 0
488
- ? "completed"
489
- : "failed";
683
+ progress.status = singleResult?.paused
684
+ ? "paused"
685
+ : singleResult?.aborted
686
+ ? "aborted"
687
+ : singleResult?.status === "completed"
688
+ ? "completed"
689
+ : "failed";
490
690
  progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
491
691
  progress.tokens = singleResult?.tokens ?? 0;
492
692
  progress.contextTokens = singleResult?.contextTokens;
493
693
  progress.contextWindow = singleResult?.contextWindow;
494
694
  progress.cost = singleResult?.usage?.cost.total ?? 0;
495
- progress.extractedToolData = singleResult?.extractedToolData;
496
- progress.retryFailure = singleResult?.retryFailure;
695
+ progress.extractedToolData = undefined;
696
+ progress.retryFailure = singleResult?.retryFailure
697
+ ? {
698
+ attempt: singleResult.retryFailure.attempt,
699
+ errorMessage: singleResult.retryFailure.errorSummary,
700
+ }
701
+ : undefined;
497
702
  progress.retryState = undefined;
498
703
  }
499
704
  completedJobs += 1;
@@ -517,6 +722,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
517
722
  `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`,
518
723
  );
519
724
  }
725
+ if (singleResult?.paused) {
726
+ return { kind: "paused" };
727
+ }
520
728
  return finalText;
521
729
  } catch (error) {
522
730
  if (progress) {
@@ -568,6 +776,31 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
568
776
  },
569
777
  );
570
778
  startedJobs.push({ jobId, taskId: taskItem.id });
779
+ if (typeof manager.registerResumeDescriptor === "function") {
780
+ manager.registerResumeDescriptor({
781
+ subagentId: uniqueId,
782
+ ownerId: this.session.getAgentId?.() ?? undefined,
783
+ data: {
784
+ toolCallId: _toolCallId,
785
+ params,
786
+ task: { ...taskItem, id: uniqueId },
787
+ sessionFile: subtaskSessionFile,
788
+ forkContextSeed: frozenForkSeed,
789
+ agentSource: fallbackAgentSource,
790
+ } satisfies TaskResumeDescriptor,
791
+ });
792
+ }
793
+ if (typeof manager.registerSubagentRecord === "function") {
794
+ manager.registerSubagentRecord({
795
+ subagentId: uniqueId,
796
+ ownerId: this.session.getAgentId?.() ?? undefined,
797
+ currentJobId: jobId,
798
+ historicalJobIds: [],
799
+ status: manager.getJob(jobId)?.status ?? "running",
800
+ sessionFile: subtaskSessionFile,
801
+ resumable: !!batchArtifactsDir,
802
+ });
803
+ }
571
804
  } catch (error) {
572
805
  const message = error instanceof Error ? error.message : String(error);
573
806
  failedSchedules.push(`${taskItem.id}: ${message}`);
@@ -637,6 +870,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
637
870
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
638
871
  preAllocatedIds?: string[],
639
872
  prebuiltForkContextSeeds?: ReadonlyMap<string, ForkContextSeed>,
873
+ executionOverrides?: {
874
+ runMode?: "initial" | "resume" | "message";
875
+ resumeMessage?: string;
876
+ sessionFiles?: ReadonlyMap<string, string | null>;
877
+ },
640
878
  ): Promise<AgentToolResult<TaskToolDetails>> {
641
879
  const startTime = Date.now();
642
880
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
@@ -818,6 +1056,44 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
818
1056
  };
819
1057
  }
820
1058
 
1059
+ const batchGateDecision = evaluateSpawnGate({ childCount: tasks.length, plan: params.spawnPlan });
1060
+ if (batchGateDecision.outcome === "rejected") {
1061
+ return {
1062
+ content: [
1063
+ {
1064
+ type: "text",
1065
+ text: `Task spawn gate rejected this batch: ${batchGateDecision.reason}. Batches with more than ${DEFAULT_SPAWN_THRESHOLD} tasks require spawnPlan fields: ${batchGateDecision.missingFields.join(", ")}.`,
1066
+ },
1067
+ ],
1068
+ details: {
1069
+ projectAgentsDir,
1070
+ results: [],
1071
+ totalDurationMs: Date.now() - startTime,
1072
+ },
1073
+ };
1074
+ }
1075
+
1076
+ const reviewerExploreDecision = evaluateReviewerExploreGate({
1077
+ spawningAgentType: this.#spawningAgentType,
1078
+ targetAgent: agentName,
1079
+ plan: params.spawnPlan,
1080
+ });
1081
+ if (reviewerExploreDecision.outcome === "rejected") {
1082
+ return {
1083
+ content: [
1084
+ {
1085
+ type: "text",
1086
+ text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
1087
+ },
1088
+ ],
1089
+ details: {
1090
+ projectAgentsDir,
1091
+ results: [],
1092
+ totalDurationMs: Date.now() - startTime,
1093
+ },
1094
+ };
1095
+ }
1096
+
821
1097
  let repoRoot: string | null = null;
822
1098
  let baseline: WorktreeBaseline | null = null;
823
1099
  if (isIsolated) {
@@ -943,7 +1219,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
943
1219
  this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
944
1220
  uniqueIds = await outputManager.allocateBatch(tasks.map(t => t.id));
945
1221
  }
946
- const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
1222
+ const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: validateAllocatedTaskId(uniqueIds[i] ?? "") }));
947
1223
 
948
1224
  const availableSkills = [...(this.session.skills ?? [])];
949
1225
  // Resolve autoload skills from agent definition against available skills
@@ -983,23 +1259,43 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
983
1259
  emitProgress();
984
1260
 
985
1261
  const buildForkContextSeed = async (task: (typeof tasksWithUniqueIds)[number]) => {
986
- if (task.inheritContext !== true) return undefined;
1262
+ if (!requestsForkContext(task)) return undefined;
987
1263
  if (!this.session.buildForkContextSeed) {
988
1264
  throw new Error("Current session cannot build fork-context seeds.");
989
1265
  }
990
- const maxMessages = this.session.settings.get("task.forkContext.maxMessages");
1266
+ const configuredMaxMessages = this.session.settings.has("task.forkContext.maxMessages")
1267
+ ? this.session.settings.get("task.forkContext.maxMessages")
1268
+ : undefined;
991
1269
  const configuredMaxTokens = this.session.settings.get("task.forkContext.maxTokens");
1270
+ const params = resolveForkSeedParamsForMode(
1271
+ task.inheritContext,
1272
+ configuredMaxMessages,
1273
+ configuredMaxTokens,
1274
+ this.session.model,
1275
+ );
1276
+ if (!params) return undefined;
992
1277
  return await this.session.buildForkContextSeed({
993
- maxMessages,
994
- maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, this.session.model),
1278
+ ...params,
995
1279
  signal,
996
1280
  });
997
1281
  };
998
1282
 
999
- const runTask = async (task: (typeof tasksWithUniqueIds)[number], index: number) => {
1283
+ const runTask = async (
1284
+ task: (typeof tasksWithUniqueIds)[number],
1285
+ index: number,
1286
+ overrides?: {
1287
+ runMode?: "initial" | "resume" | "message";
1288
+ resumeMessage?: string;
1289
+ sessionFile?: string | null;
1290
+ },
1291
+ ) => {
1000
1292
  const forkContextSeed = prebuiltForkContextSeeds?.get(task.id) ?? (await buildForkContextSeed(task));
1293
+ const forkContext = requestsForkContext(task)
1294
+ ? { mode: task.inheritContext, clonedTokens: forkContextSeed?.metadata.approximateTokens ?? 0 }
1295
+ : undefined;
1296
+ const taskSessionFile = overrides?.sessionFile ?? executionOverrides?.sessionFiles?.get(task.id) ?? null;
1001
1297
  if (!isIsolated) {
1002
- return runSubprocess({
1298
+ const result = await runSubprocess({
1003
1299
  cwd: this.session.cwd,
1004
1300
  agent: effectiveAgent,
1005
1301
  task: renderSubagentUserPrompt(task.assignment, simpleMode),
@@ -1008,12 +1304,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1008
1304
  description: task.description,
1009
1305
  index,
1010
1306
  id: task.id,
1307
+ runMode: overrides?.runMode ?? executionOverrides?.runMode,
1308
+ resumeMessage: overrides?.resumeMessage ?? executionOverrides?.resumeMessage,
1309
+ subagentId: task.id,
1011
1310
  taskDepth,
1012
1311
  modelOverride,
1013
1312
  parentActiveModelPattern,
1014
1313
  thinkingLevel: thinkingLevelOverride,
1015
1314
  outputSchema: effectiveOutputSchema,
1016
- sessionFile,
1315
+ sessionFile: taskSessionFile,
1017
1316
  persistArtifacts: !!artifactsDir,
1018
1317
  artifactsDir: effectiveArtifactsDir,
1019
1318
  contextFile: contextFilePath,
@@ -1040,6 +1339,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1040
1339
  parentTelemetry: this.session.getTelemetry?.(),
1041
1340
  forkContextSeed,
1042
1341
  });
1342
+ return forkContext ? { ...result, forkContext } : result;
1043
1343
  }
1044
1344
 
1045
1345
  const taskStart = Date.now();
@@ -1063,12 +1363,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1063
1363
  description: task.description,
1064
1364
  index,
1065
1365
  id: task.id,
1366
+ runMode: overrides?.runMode ?? executionOverrides?.runMode,
1367
+ resumeMessage: overrides?.resumeMessage ?? executionOverrides?.resumeMessage,
1368
+ subagentId: task.id,
1066
1369
  taskDepth,
1067
1370
  modelOverride,
1068
1371
  parentActiveModelPattern,
1069
1372
  thinkingLevel: thinkingLevelOverride,
1070
1373
  outputSchema: effectiveOutputSchema,
1071
- sessionFile,
1374
+ sessionFile: taskSessionFile,
1072
1375
  persistArtifacts: !!artifactsDir,
1073
1376
  artifactsDir: effectiveArtifactsDir,
1074
1377
  contextFile: contextFilePath,
@@ -1095,7 +1398,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1095
1398
  parentTelemetry: this.session.getTelemetry?.(),
1096
1399
  forkContextSeed,
1097
1400
  });
1098
- if (mergeMode === "branch" && result.exitCode === 0) {
1401
+ const resultWithForkContext = forkContext ? { ...result, forkContext } : result;
1402
+ if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
1099
1403
  try {
1100
1404
  const commitMsg =
1101
1405
  commitStyle === "ai" && this.session.modelRegistry
@@ -1115,35 +1419,40 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1115
1419
  task.description,
1116
1420
  commitMsg,
1117
1421
  );
1422
+ const producedChanges = Boolean(commitResult?.branchName || commitResult?.nestedPatches.length);
1118
1423
  return {
1119
- ...result,
1424
+ ...resultWithForkContext,
1120
1425
  branchName: commitResult?.branchName,
1121
1426
  nestedPatches: commitResult?.nestedPatches,
1427
+ producedChanges,
1122
1428
  };
1123
1429
  } catch (mergeErr) {
1124
1430
  // Agent succeeded but branch commit failed — clean up stale branch
1125
1431
  const branchName = `gjc/task/${task.id}`;
1126
1432
  await git.branch.tryDelete(repoRoot, branchName);
1127
1433
  const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
1128
- return { ...result, error: `Merge failed: ${msg}` };
1434
+ return { ...resultWithForkContext, error: `Merge failed: ${msg}` };
1129
1435
  }
1130
1436
  }
1131
- if (result.exitCode === 0) {
1437
+ if (resultWithForkContext.exitCode === 0) {
1132
1438
  try {
1133
1439
  const delta = await captureDeltaPatch(isolationDir, taskBaseline);
1134
- const patchPath = path.join(effectiveArtifactsDir, `${task.id}.patch`);
1440
+ const artifactId = validateAllocatedTaskId(task.id);
1441
+ const patchPath = path.join(effectiveArtifactsDir, `${artifactId}.patch`);
1135
1442
  await Bun.write(patchPath, delta.rootPatch);
1443
+ const producedChanges = Boolean(delta.rootPatch.trim() || delta.nestedPatches.length);
1136
1444
  return {
1137
- ...result,
1445
+ ...resultWithForkContext,
1138
1446
  patchPath,
1139
1447
  nestedPatches: delta.nestedPatches,
1448
+ producedChanges,
1140
1449
  };
1141
1450
  } catch (patchErr) {
1142
1451
  const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
1143
- return { ...result, error: `Patch capture failed: ${msg}` };
1452
+ return { ...resultWithForkContext, error: `Patch capture failed: ${msg}` };
1144
1453
  }
1145
1454
  }
1146
- return result;
1455
+ return resultWithForkContext;
1147
1456
  } catch (err) {
1148
1457
  const message = err instanceof Error ? err.message : String(err);
1149
1458
  const assignment = task.assignment.trim();
@@ -1162,6 +1471,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1162
1471
  durationMs: Date.now() - taskStart,
1163
1472
  tokens: 0,
1164
1473
  modelOverride,
1474
+ forkContext,
1165
1475
  error: message,
1166
1476
  };
1167
1477
  } finally {
@@ -1206,6 +1516,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1206
1516
  abortReason: "Cancelled before start",
1207
1517
  };
1208
1518
  });
1519
+ if (!artifactsDir) {
1520
+ for (const result of results) {
1521
+ delete result.outputMeta;
1522
+ delete result.outputPath;
1523
+ }
1524
+ }
1525
+
1526
+ const forkContextClonedTokens = results.reduce(
1527
+ (total, result) => total + (result.forkContext?.clonedTokens ?? 0),
1528
+ 0,
1529
+ );
1209
1530
 
1210
1531
  // Aggregate usage from executor results (already accumulated incrementally)
1211
1532
  const aggregatedUsage = createUsageTotals();
@@ -1217,13 +1538,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1217
1538
  }
1218
1539
  }
1219
1540
 
1220
- // Collect output paths (artifacts already written by executor in real-time)
1221
- const outputPaths: string[] = [];
1222
1541
  const patchPaths: string[] = [];
1223
1542
  for (const result of results) {
1224
- if (result.outputPath) {
1225
- outputPaths.push(result.outputPath);
1226
- }
1227
1543
  if (result.patchPath) {
1228
1544
  patchPaths.push(result.patchPath);
1229
1545
  }
@@ -1319,7 +1635,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1319
1635
  "<system-notification>Patches were not applied and must be handled manually.</system-notification>";
1320
1636
  const patchList =
1321
1637
  patchPaths.length > 0
1322
- ? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
1638
+ ? `\n\nPatch artifacts: ${patchPaths.length} preserved for internal merge recovery.`
1323
1639
  : "";
1324
1640
  mergeSummary = `\n\n${notification}${patchList}`;
1325
1641
  }
@@ -1375,41 +1691,25 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1375
1691
  const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
1376
1692
  const totalDuration = Date.now() - startTime;
1377
1693
 
1378
- const summaries = results.map(r => {
1379
- const status = r.aborted
1380
- ? "cancelled"
1381
- : r.exitCode === 0 && r.error
1382
- ? "merge failed"
1383
- : r.exitCode === 0
1384
- ? "completed"
1385
- : `failed (exit ${r.exitCode})`;
1386
- const output = r.output.trim() || r.stderr.trim() || "(no output)";
1387
- const outputCharCount = r.outputMeta?.charCount ?? output.length;
1388
- const fullOutputThreshold = 5000;
1389
- let preview = output;
1390
- let truncated = false;
1391
- if (outputCharCount > fullOutputThreshold) {
1392
- const slice = output.slice(0, fullOutputThreshold);
1393
- const lastNewline = slice.lastIndexOf("\n");
1394
- preview = lastNewline >= 0 ? slice.slice(0, lastNewline) : slice;
1395
- truncated = true;
1396
- }
1694
+ const receipts = results.map(buildTaskReceipt);
1695
+ const roiSummary = buildTaskRoiSummary(receipts);
1696
+ const summaries = receipts.map(r => {
1697
+ const status = r.status === "merge_failed" ? "merge failed" : r.status;
1397
1698
  return {
1398
1699
  agent: r.agent,
1399
1700
  status,
1400
1701
  id: r.id,
1401
- preview,
1402
- truncated,
1403
- meta: r.outputMeta
1702
+ synopsis: r.preview,
1703
+ outputUri: r.outputRef?.uri,
1704
+ meta: r.outputRef
1404
1705
  ? {
1405
- lineCount: r.outputMeta.lineCount,
1406
- charSize: formatBytes(r.outputMeta.charCount),
1706
+ lineCount: r.outputRef.lineCount,
1707
+ charSize: formatBytes(r.outputRef.sizeBytes),
1407
1708
  }
1408
1709
  : undefined,
1409
1710
  };
1410
1711
  });
1411
1712
 
1412
- const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
1413
1713
  const summary = prompt.render(taskSummaryTemplate, {
1414
1714
  successCount,
1415
1715
  totalCount: results.length,
@@ -1417,7 +1717,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1417
1717
  hasCancelledNote: aborted && cancelledCount > 0,
1418
1718
  duration: formatDuration(totalDuration),
1419
1719
  summaries,
1420
- outputIds,
1421
1720
  agentName,
1422
1721
  mergeSummary,
1423
1722
  });
@@ -1429,15 +1728,18 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1429
1728
  await fs.rm(tempArtifactsDir, { recursive: true, force: true });
1430
1729
  }
1431
1730
 
1731
+ const details: TaskToolDetails = {
1732
+ projectAgentsDir,
1733
+ results: receipts,
1734
+ totalDurationMs: totalDuration,
1735
+ usage: hasAggregatedUsage ? aggregatedUsage : undefined,
1736
+ forkContextClonedTokens: forkContextClonedTokens > 0 ? forkContextClonedTokens : undefined,
1737
+ roiSummary,
1738
+ };
1739
+ assertNoRawTaskFields(details, "task.return.details");
1432
1740
  return {
1433
1741
  content: [{ type: "text", text: summary }],
1434
- details: {
1435
- projectAgentsDir,
1436
- results: results,
1437
- totalDurationMs: totalDuration,
1438
- usage: hasAggregatedUsage ? aggregatedUsage : undefined,
1439
- outputPaths,
1440
- },
1742
+ details,
1441
1743
  };
1442
1744
  } catch (err) {
1443
1745
  return {