@gajae-code/coding-agent 0.3.0 → 0.3.2

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 (213) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +3 -1
  5. package/dist/types/commands/deep-interview.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +6 -0
  7. package/dist/types/config/keybindings.d.ts +5 -0
  8. package/dist/types/config/model-profile-activation.d.ts +30 -0
  9. package/dist/types/config/model-profiles.d.ts +19 -0
  10. package/dist/types/config/model-registry.d.ts +8 -0
  11. package/dist/types/config/model-resolver.d.ts +1 -1
  12. package/dist/types/config/models-config-schema.d.ts +47 -0
  13. package/dist/types/config/settings-schema.d.ts +14 -4
  14. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  15. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  16. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  17. package/dist/types/eval/py/executor.d.ts +2 -0
  18. package/dist/types/eval/py/kernel.d.ts +2 -0
  19. package/dist/types/exec/bash-executor.d.ts +10 -0
  20. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  21. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  22. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  23. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  24. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  25. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
  26. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  27. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  28. package/dist/types/hooks/skill-state.d.ts +21 -0
  29. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  32. package/dist/types/internal-urls/types.d.ts +4 -0
  33. package/dist/types/lsp/index.d.ts +10 -10
  34. package/dist/types/main.d.ts +10 -1
  35. package/dist/types/modes/bridge/auth.d.ts +12 -0
  36. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  37. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  38. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  39. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  41. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  43. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  44. package/dist/types/modes/components/model-selector.d.ts +6 -1
  45. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  46. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  47. package/dist/types/modes/components/status-line.d.ts +2 -0
  48. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  49. package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
  50. package/dist/types/modes/index.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +1 -0
  52. package/dist/types/modes/jobs-observer.d.ts +57 -0
  53. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  54. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  55. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  56. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  57. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  58. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  59. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  60. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  61. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  62. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  63. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  64. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  65. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  66. package/dist/types/modes/types.d.ts +2 -0
  67. package/dist/types/sdk.d.ts +3 -1
  68. package/dist/types/session/agent-session.d.ts +11 -1
  69. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  70. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  71. package/dist/types/task/executor.d.ts +1 -0
  72. package/dist/types/task/id.d.ts +7 -0
  73. package/dist/types/task/index.d.ts +5 -0
  74. package/dist/types/task/receipt.d.ts +85 -0
  75. package/dist/types/task/spawn-gate.d.ts +38 -0
  76. package/dist/types/task/types.d.ts +143 -11
  77. package/dist/types/tools/cron.d.ts +6 -0
  78. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  79. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  80. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  81. package/dist/types/tools/index.d.ts +6 -4
  82. package/dist/types/tools/path-utils.d.ts +1 -0
  83. package/dist/types/tools/subagent.d.ts +15 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +7 -0
  86. package/src/async/job-manager.ts +36 -0
  87. package/src/cli/args.ts +19 -2
  88. package/src/commands/deep-interview.ts +1 -0
  89. package/src/commands/harness.ts +289 -19
  90. package/src/commands/launch.ts +10 -2
  91. package/src/commands/state.ts +2 -1
  92. package/src/commands/team.ts +22 -4
  93. package/src/config/keybindings.ts +6 -0
  94. package/src/config/model-profile-activation.ts +157 -0
  95. package/src/config/model-profiles.ts +155 -0
  96. package/src/config/model-registry.ts +19 -0
  97. package/src/config/model-resolver.ts +3 -2
  98. package/src/config/models-config-schema.ts +36 -0
  99. package/src/config/settings-schema.ts +16 -3
  100. package/src/dap/client.ts +17 -3
  101. package/src/debug/crash-diagnostics.ts +223 -0
  102. package/src/debug/runtime-gauges.ts +20 -0
  103. package/src/deep-interview/render-middleware.ts +6 -0
  104. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  105. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  106. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
  107. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  108. package/src/defaults/gjc-defaults.ts +7 -0
  109. package/src/eval/py/executor.ts +21 -1
  110. package/src/eval/py/kernel.ts +15 -0
  111. package/src/exec/bash-executor.ts +41 -0
  112. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  113. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  114. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  115. package/src/gjc-runtime/state-migrations.ts +54 -7
  116. package/src/gjc-runtime/state-runtime.ts +461 -64
  117. package/src/gjc-runtime/state-schema.ts +192 -0
  118. package/src/gjc-runtime/state-writer.ts +32 -1
  119. package/src/gjc-runtime/team-runtime.ts +177 -105
  120. package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
  121. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  122. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  123. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  124. package/src/harness-control-plane/control-endpoint.ts +19 -8
  125. package/src/harness-control-plane/owner.ts +57 -10
  126. package/src/harness-control-plane/state-machine.ts +2 -1
  127. package/src/hooks/skill-state.ts +176 -26
  128. package/src/internal-urls/agent-protocol.ts +68 -21
  129. package/src/internal-urls/artifact-protocol.ts +12 -17
  130. package/src/internal-urls/docs-index.generated.ts +8 -10
  131. package/src/internal-urls/registry-helpers.ts +19 -16
  132. package/src/internal-urls/types.ts +4 -0
  133. package/src/lsp/client.ts +18 -2
  134. package/src/main.ts +88 -6
  135. package/src/modes/bridge/auth.ts +41 -0
  136. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  137. package/src/modes/bridge/bridge-mode.ts +520 -0
  138. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  139. package/src/modes/bridge/event-stream.ts +70 -0
  140. package/src/modes/components/custom-editor.ts +101 -0
  141. package/src/modes/components/custom-provider-wizard.ts +318 -0
  142. package/src/modes/components/hook-selector.ts +61 -18
  143. package/src/modes/components/jobs-overlay-model.ts +109 -0
  144. package/src/modes/components/jobs-overlay.ts +172 -0
  145. package/src/modes/components/model-selector.ts +108 -18
  146. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  147. package/src/modes/components/status-line/presets.ts +7 -5
  148. package/src/modes/components/status-line/segments.ts +25 -0
  149. package/src/modes/components/status-line/types.ts +2 -0
  150. package/src/modes/components/status-line.ts +9 -1
  151. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  152. package/src/modes/controllers/input-controller.ts +97 -9
  153. package/src/modes/controllers/selector-controller.ts +86 -1
  154. package/src/modes/index.ts +1 -0
  155. package/src/modes/interactive-mode.ts +27 -0
  156. package/src/modes/jobs-observer.ts +204 -0
  157. package/src/modes/rpc/host-tools.ts +1 -186
  158. package/src/modes/rpc/host-uris.ts +1 -235
  159. package/src/modes/rpc/rpc-client.ts +25 -10
  160. package/src/modes/rpc/rpc-mode.ts +12 -381
  161. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  162. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  163. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  164. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  165. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  166. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  167. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  168. package/src/modes/shared/agent-wire/responses.ts +17 -0
  169. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  170. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  171. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  172. package/src/modes/types.ts +2 -0
  173. package/src/prompts/memories/consolidation.md +1 -1
  174. package/src/prompts/memories/read-path.md +6 -7
  175. package/src/prompts/memories/unavailable.md +2 -2
  176. package/src/prompts/tools/bash.md +1 -1
  177. package/src/prompts/tools/irc.md +1 -1
  178. package/src/prompts/tools/read.md +2 -2
  179. package/src/prompts/tools/recall.md +1 -0
  180. package/src/prompts/tools/reflect.md +1 -0
  181. package/src/prompts/tools/retain.md +1 -0
  182. package/src/prompts/tools/subagent.md +12 -7
  183. package/src/prompts/tools/task-summary.md +3 -9
  184. package/src/prompts/tools/task.md +5 -1
  185. package/src/sdk.ts +5 -1
  186. package/src/session/agent-session.ts +214 -38
  187. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  188. package/src/skill-state/workflow-state-contract.ts +7 -4
  189. package/src/skill-state/workflow-state-version.ts +3 -0
  190. package/src/slash-commands/builtin-registry.ts +9 -1
  191. package/src/task/executor.ts +31 -5
  192. package/src/task/id.ts +33 -0
  193. package/src/task/index.ts +259 -67
  194. package/src/task/output-manager.ts +5 -4
  195. package/src/task/receipt.ts +297 -0
  196. package/src/task/render.ts +48 -131
  197. package/src/task/spawn-gate.ts +132 -0
  198. package/src/task/types.ts +48 -7
  199. package/src/tools/ask.ts +73 -33
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash.ts +1 -1
  203. package/src/tools/cron.ts +48 -0
  204. package/src/tools/find.ts +4 -1
  205. package/src/tools/hindsight-recall.ts +0 -2
  206. package/src/tools/hindsight-reflect.ts +0 -2
  207. package/src/tools/hindsight-retain.ts +0 -2
  208. package/src/tools/index.ts +6 -18
  209. package/src/tools/path-utils.ts +3 -2
  210. package/src/tools/read.ts +4 -3
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/skill.ts +6 -1
  213. package/src/tools/subagent.ts +237 -84
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,
@@ -122,11 +126,37 @@ function addUsageTotals(target: Usage, usage: Partial<Usage>): void {
122
126
  target.cost.total += cost.total;
123
127
  }
124
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
+
125
138
  // Re-export types and utilities
126
139
  export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
127
140
  export { discoverCommands, expandCommand, getCommand } from "./commands";
128
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";
129
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";
130
160
  export type {
131
161
  AgentDefinition,
132
162
  AgentProgress,
@@ -203,6 +233,14 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
203
233
  if (!customSchemaEnabled && params.schema !== undefined) {
204
234
  disallowedFields.push("schema");
205
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
+ }
206
244
  if (disallowedFields.length === 0) {
207
245
  return undefined;
208
246
  }
@@ -212,22 +250,77 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
212
250
  }
213
251
 
214
252
  if (disallowedFields.length === 1) {
215
- 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.`;
216
254
  }
217
255
 
218
- 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.`;
219
257
  }
220
258
 
221
259
  function getForkContextPolicy(agent: AgentDefinition): ForkContextPolicy {
222
260
  return agent.forkContext ?? "forbidden";
223
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
+ }
224
311
 
225
312
  function validateForkContextRequests(
226
313
  tasks: readonly TaskItem[],
227
314
  agent: AgentDefinition,
228
315
  forkContextEnabled: boolean,
229
316
  ): string | undefined {
230
- 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);
231
324
  if (requested.length === 0) return undefined;
232
325
  const taskIds = requested.map(task => task.id).join(", ");
233
326
  if (!forkContextEnabled) {
@@ -239,10 +332,10 @@ function validateForkContextRequests(
239
332
  return undefined;
240
333
  }
241
334
 
242
- function resolveForkContextMaxTokens(configured: number, model: Model | undefined): number {
335
+ export function resolveForkContextMaxTokens(configured: number, model: Model | undefined): number {
243
336
  if (configured > 0) return Math.trunc(configured);
244
337
  const contextWindow = model?.contextWindow ?? 0;
245
- 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;
246
339
  }
247
340
 
248
341
  // ═══════════════════════════════════════════════════════════════════════════
@@ -264,6 +357,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
264
357
  readonly renderResult = renderResult;
265
358
  readonly #discoveredAgents: AgentDefinition[];
266
359
  readonly #blockedAgent: string | undefined;
360
+ readonly #spawningAgentType: string | undefined;
267
361
 
268
362
  get parameters(): TaskToolSchemaInstance {
269
363
  const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
@@ -295,6 +389,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
295
389
  discoveredAgents: AgentDefinition[],
296
390
  ) {
297
391
  this.#blockedAgent = $env.PI_BLOCKED_AGENT;
392
+ this.#spawningAgentType = session.currentAgentType;
298
393
  this.#discoveredAgents = discoveredAgents;
299
394
  }
300
395
 
@@ -324,10 +419,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
324
419
  }
325
420
 
326
421
  const taskItems = params.tasks ?? [];
422
+ const taskIdValidationError = validateTaskIdsForScheduling(taskItems);
423
+ if (taskIdValidationError) {
424
+ return createTaskModeError(taskIdValidationError);
425
+ }
327
426
  if (taskItems.length === 0) {
328
427
  return this.#executeSync(_toolCallId, params, signal, onUpdate);
329
428
  }
330
-
331
429
  const agent = getAgent(this.#discoveredAgents, params.agent);
332
430
  if (!agent) {
333
431
  const available =
@@ -365,6 +463,36 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
365
463
  return createTaskModeError(forkContextValidationError);
366
464
  }
367
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
+
368
496
  const manager = AsyncJobManager.instance();
369
497
  if (!manager) {
370
498
  return {
@@ -474,15 +602,23 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
474
602
  }
475
603
  const semaphore = new Semaphore(maxConcurrency);
476
604
  const buildForkContextSeedForTask = async (task: TaskItem): Promise<ForkContextSeed | undefined> => {
477
- if (task.inheritContext !== true) return undefined;
605
+ if (!requestsForkContext(task)) return undefined;
478
606
  if (!this.session.buildForkContextSeed) {
479
607
  throw new Error("Current session cannot build fork-context seeds.");
480
608
  }
481
- 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;
482
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;
483
620
  return await this.session.buildForkContextSeed({
484
- maxMessages,
485
- maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, this.session.model),
621
+ ...params,
486
622
  signal,
487
623
  });
488
624
  };
@@ -501,7 +637,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
501
637
  continue;
502
638
  }
503
639
 
504
- const uniqueId = uniqueIds[i];
640
+ const uniqueId = validateAllocatedTaskId(uniqueIds[i] ?? "");
505
641
  const frozenForkSeed = await buildForkContextSeedForTask(taskItem);
506
642
  if (frozenForkSeed) frozenForkSeeds.set(uniqueId, frozenForkSeed);
507
643
  const singleParams: TaskParams = { ...params, tasks: [taskItem] };
@@ -548,7 +684,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
548
684
  ? "paused"
549
685
  : singleResult?.aborted
550
686
  ? "aborted"
551
- : (singleResult?.exitCode ?? 0) === 0
687
+ : singleResult?.status === "completed"
552
688
  ? "completed"
553
689
  : "failed";
554
690
  progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
@@ -556,8 +692,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
556
692
  progress.contextTokens = singleResult?.contextTokens;
557
693
  progress.contextWindow = singleResult?.contextWindow;
558
694
  progress.cost = singleResult?.usage?.cost.total ?? 0;
559
- progress.extractedToolData = singleResult?.extractedToolData;
560
- 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;
561
702
  progress.retryState = undefined;
562
703
  }
563
704
  completedJobs += 1;
@@ -915,6 +1056,44 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
915
1056
  };
916
1057
  }
917
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
+
918
1097
  let repoRoot: string | null = null;
919
1098
  let baseline: WorktreeBaseline | null = null;
920
1099
  if (isIsolated) {
@@ -1040,7 +1219,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1040
1219
  this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
1041
1220
  uniqueIds = await outputManager.allocateBatch(tasks.map(t => t.id));
1042
1221
  }
1043
- const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
1222
+ const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: validateAllocatedTaskId(uniqueIds[i] ?? "") }));
1044
1223
 
1045
1224
  const availableSkills = [...(this.session.skills ?? [])];
1046
1225
  // Resolve autoload skills from agent definition against available skills
@@ -1080,15 +1259,23 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1080
1259
  emitProgress();
1081
1260
 
1082
1261
  const buildForkContextSeed = async (task: (typeof tasksWithUniqueIds)[number]) => {
1083
- if (task.inheritContext !== true) return undefined;
1262
+ if (!requestsForkContext(task)) return undefined;
1084
1263
  if (!this.session.buildForkContextSeed) {
1085
1264
  throw new Error("Current session cannot build fork-context seeds.");
1086
1265
  }
1087
- 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;
1088
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;
1089
1277
  return await this.session.buildForkContextSeed({
1090
- maxMessages,
1091
- maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, this.session.model),
1278
+ ...params,
1092
1279
  signal,
1093
1280
  });
1094
1281
  };
@@ -1103,9 +1290,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1103
1290
  },
1104
1291
  ) => {
1105
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;
1106
1296
  const taskSessionFile = overrides?.sessionFile ?? executionOverrides?.sessionFiles?.get(task.id) ?? null;
1107
1297
  if (!isIsolated) {
1108
- return runSubprocess({
1298
+ const result = await runSubprocess({
1109
1299
  cwd: this.session.cwd,
1110
1300
  agent: effectiveAgent,
1111
1301
  task: renderSubagentUserPrompt(task.assignment, simpleMode),
@@ -1120,6 +1310,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1120
1310
  taskDepth,
1121
1311
  modelOverride,
1122
1312
  parentActiveModelPattern,
1313
+ parentSessionId: this.session.getSessionId?.() ?? undefined,
1123
1314
  thinkingLevel: thinkingLevelOverride,
1124
1315
  outputSchema: effectiveOutputSchema,
1125
1316
  sessionFile: taskSessionFile,
@@ -1149,6 +1340,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1149
1340
  parentTelemetry: this.session.getTelemetry?.(),
1150
1341
  forkContextSeed,
1151
1342
  });
1343
+ return forkContext ? { ...result, forkContext } : result;
1152
1344
  }
1153
1345
 
1154
1346
  const taskStart = Date.now();
@@ -1178,6 +1370,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1178
1370
  taskDepth,
1179
1371
  modelOverride,
1180
1372
  parentActiveModelPattern,
1373
+ parentSessionId: this.session.getSessionId?.() ?? undefined,
1181
1374
  thinkingLevel: thinkingLevelOverride,
1182
1375
  outputSchema: effectiveOutputSchema,
1183
1376
  sessionFile: taskSessionFile,
@@ -1207,7 +1400,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1207
1400
  parentTelemetry: this.session.getTelemetry?.(),
1208
1401
  forkContextSeed,
1209
1402
  });
1210
- if (mergeMode === "branch" && result.exitCode === 0) {
1403
+ const resultWithForkContext = forkContext ? { ...result, forkContext } : result;
1404
+ if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
1211
1405
  try {
1212
1406
  const commitMsg =
1213
1407
  commitStyle === "ai" && this.session.modelRegistry
@@ -1227,35 +1421,40 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1227
1421
  task.description,
1228
1422
  commitMsg,
1229
1423
  );
1424
+ const producedChanges = Boolean(commitResult?.branchName || commitResult?.nestedPatches.length);
1230
1425
  return {
1231
- ...result,
1426
+ ...resultWithForkContext,
1232
1427
  branchName: commitResult?.branchName,
1233
1428
  nestedPatches: commitResult?.nestedPatches,
1429
+ producedChanges,
1234
1430
  };
1235
1431
  } catch (mergeErr) {
1236
1432
  // Agent succeeded but branch commit failed — clean up stale branch
1237
1433
  const branchName = `gjc/task/${task.id}`;
1238
1434
  await git.branch.tryDelete(repoRoot, branchName);
1239
1435
  const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
1240
- return { ...result, error: `Merge failed: ${msg}` };
1436
+ return { ...resultWithForkContext, error: `Merge failed: ${msg}` };
1241
1437
  }
1242
1438
  }
1243
- if (result.exitCode === 0) {
1439
+ if (resultWithForkContext.exitCode === 0) {
1244
1440
  try {
1245
1441
  const delta = await captureDeltaPatch(isolationDir, taskBaseline);
1246
- const patchPath = path.join(effectiveArtifactsDir, `${task.id}.patch`);
1442
+ const artifactId = validateAllocatedTaskId(task.id);
1443
+ const patchPath = path.join(effectiveArtifactsDir, `${artifactId}.patch`);
1247
1444
  await Bun.write(patchPath, delta.rootPatch);
1445
+ const producedChanges = Boolean(delta.rootPatch.trim() || delta.nestedPatches.length);
1248
1446
  return {
1249
- ...result,
1447
+ ...resultWithForkContext,
1250
1448
  patchPath,
1251
1449
  nestedPatches: delta.nestedPatches,
1450
+ producedChanges,
1252
1451
  };
1253
1452
  } catch (patchErr) {
1254
1453
  const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
1255
- return { ...result, error: `Patch capture failed: ${msg}` };
1454
+ return { ...resultWithForkContext, error: `Patch capture failed: ${msg}` };
1256
1455
  }
1257
1456
  }
1258
- return result;
1457
+ return resultWithForkContext;
1259
1458
  } catch (err) {
1260
1459
  const message = err instanceof Error ? err.message : String(err);
1261
1460
  const assignment = task.assignment.trim();
@@ -1274,6 +1473,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1274
1473
  durationMs: Date.now() - taskStart,
1275
1474
  tokens: 0,
1276
1475
  modelOverride,
1476
+ forkContext,
1277
1477
  error: message,
1278
1478
  };
1279
1479
  } finally {
@@ -1318,6 +1518,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1318
1518
  abortReason: "Cancelled before start",
1319
1519
  };
1320
1520
  });
1521
+ if (!artifactsDir) {
1522
+ for (const result of results) {
1523
+ delete result.outputMeta;
1524
+ delete result.outputPath;
1525
+ }
1526
+ }
1527
+
1528
+ const forkContextClonedTokens = results.reduce(
1529
+ (total, result) => total + (result.forkContext?.clonedTokens ?? 0),
1530
+ 0,
1531
+ );
1321
1532
 
1322
1533
  // Aggregate usage from executor results (already accumulated incrementally)
1323
1534
  const aggregatedUsage = createUsageTotals();
@@ -1329,13 +1540,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1329
1540
  }
1330
1541
  }
1331
1542
 
1332
- // Collect output paths (artifacts already written by executor in real-time)
1333
- const outputPaths: string[] = [];
1334
1543
  const patchPaths: string[] = [];
1335
1544
  for (const result of results) {
1336
- if (result.outputPath) {
1337
- outputPaths.push(result.outputPath);
1338
- }
1339
1545
  if (result.patchPath) {
1340
1546
  patchPaths.push(result.patchPath);
1341
1547
  }
@@ -1431,7 +1637,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1431
1637
  "<system-notification>Patches were not applied and must be handled manually.</system-notification>";
1432
1638
  const patchList =
1433
1639
  patchPaths.length > 0
1434
- ? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
1640
+ ? `\n\nPatch artifacts: ${patchPaths.length} preserved for internal merge recovery.`
1435
1641
  : "";
1436
1642
  mergeSummary = `\n\n${notification}${patchList}`;
1437
1643
  }
@@ -1487,41 +1693,25 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1487
1693
  const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
1488
1694
  const totalDuration = Date.now() - startTime;
1489
1695
 
1490
- const summaries = results.map(r => {
1491
- const status = r.aborted
1492
- ? "cancelled"
1493
- : r.exitCode === 0 && r.error
1494
- ? "merge failed"
1495
- : r.exitCode === 0
1496
- ? "completed"
1497
- : `failed (exit ${r.exitCode})`;
1498
- const output = r.output.trim() || r.stderr.trim() || "(no output)";
1499
- const outputCharCount = r.outputMeta?.charCount ?? output.length;
1500
- const fullOutputThreshold = 5000;
1501
- let preview = output;
1502
- let truncated = false;
1503
- if (outputCharCount > fullOutputThreshold) {
1504
- const slice = output.slice(0, fullOutputThreshold);
1505
- const lastNewline = slice.lastIndexOf("\n");
1506
- preview = lastNewline >= 0 ? slice.slice(0, lastNewline) : slice;
1507
- truncated = true;
1508
- }
1696
+ const receipts = results.map(buildTaskReceipt);
1697
+ const roiSummary = buildTaskRoiSummary(receipts);
1698
+ const summaries = receipts.map(r => {
1699
+ const status = r.status === "merge_failed" ? "merge failed" : r.status;
1509
1700
  return {
1510
1701
  agent: r.agent,
1511
1702
  status,
1512
1703
  id: r.id,
1513
- preview,
1514
- truncated,
1515
- meta: r.outputMeta
1704
+ synopsis: r.preview,
1705
+ outputUri: r.outputRef?.uri,
1706
+ meta: r.outputRef
1516
1707
  ? {
1517
- lineCount: r.outputMeta.lineCount,
1518
- charSize: formatBytes(r.outputMeta.charCount),
1708
+ lineCount: r.outputRef.lineCount,
1709
+ charSize: formatBytes(r.outputRef.sizeBytes),
1519
1710
  }
1520
1711
  : undefined,
1521
1712
  };
1522
1713
  });
1523
1714
 
1524
- const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
1525
1715
  const summary = prompt.render(taskSummaryTemplate, {
1526
1716
  successCount,
1527
1717
  totalCount: results.length,
@@ -1529,7 +1719,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1529
1719
  hasCancelledNote: aborted && cancelledCount > 0,
1530
1720
  duration: formatDuration(totalDuration),
1531
1721
  summaries,
1532
- outputIds,
1533
1722
  agentName,
1534
1723
  mergeSummary,
1535
1724
  });
@@ -1541,15 +1730,18 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1541
1730
  await fs.rm(tempArtifactsDir, { recursive: true, force: true });
1542
1731
  }
1543
1732
 
1733
+ const details: TaskToolDetails = {
1734
+ projectAgentsDir,
1735
+ results: receipts,
1736
+ totalDurationMs: totalDuration,
1737
+ usage: hasAggregatedUsage ? aggregatedUsage : undefined,
1738
+ forkContextClonedTokens: forkContextClonedTokens > 0 ? forkContextClonedTokens : undefined,
1739
+ roiSummary,
1740
+ };
1741
+ assertNoRawTaskFields(details, "task.return.details");
1544
1742
  return {
1545
1743
  content: [{ type: "text", text: summary }],
1546
- details: {
1547
- projectAgentsDir,
1548
- results: results,
1549
- totalDurationMs: totalDuration,
1550
- usage: hasAggregatedUsage ? aggregatedUsage : undefined,
1551
- outputPaths,
1552
- },
1744
+ details,
1553
1745
  };
1554
1746
  } catch (err) {
1555
1747
  return {
@@ -8,6 +8,7 @@
8
8
  * This enables reliable agent:// URL resolution and prevents artifact collisions.
9
9
  */
10
10
  import * as fs from "node:fs/promises";
11
+ import { validateAllocatedTaskId, validateTaskId } from "./id";
11
12
 
12
13
  function escapeRegExp(value: string): string {
13
14
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -73,8 +74,8 @@ export class AgentOutputManager {
73
74
  */
74
75
  async allocate(id: string): Promise<string> {
75
76
  await this.#ensureInitialized();
76
- const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : "";
77
- return `${prefix}${this.#nextId++}-${id}`;
77
+ const prefix = this.#parentPrefix ? `${validateAllocatedTaskId(this.#parentPrefix)}.` : "";
78
+ return `${prefix}${this.#nextId++}-${validateTaskId(id)}`;
78
79
  }
79
80
 
80
81
  /**
@@ -85,8 +86,8 @@ export class AgentOutputManager {
85
86
  */
86
87
  async allocateBatch(ids: string[]): Promise<string[]> {
87
88
  await this.#ensureInitialized();
88
- const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : "";
89
- return ids.map(id => `${prefix}${this.#nextId++}-${id}`);
89
+ const prefix = this.#parentPrefix ? `${validateAllocatedTaskId(this.#parentPrefix)}.` : "";
90
+ return ids.map(id => `${prefix}${this.#nextId++}-${validateTaskId(id)}`);
90
91
  }
91
92
 
92
93
  /**