@gajae-code/coding-agent 0.3.0 → 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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  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/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. 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),
@@ -1149,6 +1339,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1149
1339
  parentTelemetry: this.session.getTelemetry?.(),
1150
1340
  forkContextSeed,
1151
1341
  });
1342
+ return forkContext ? { ...result, forkContext } : result;
1152
1343
  }
1153
1344
 
1154
1345
  const taskStart = Date.now();
@@ -1207,7 +1398,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1207
1398
  parentTelemetry: this.session.getTelemetry?.(),
1208
1399
  forkContextSeed,
1209
1400
  });
1210
- if (mergeMode === "branch" && result.exitCode === 0) {
1401
+ const resultWithForkContext = forkContext ? { ...result, forkContext } : result;
1402
+ if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
1211
1403
  try {
1212
1404
  const commitMsg =
1213
1405
  commitStyle === "ai" && this.session.modelRegistry
@@ -1227,35 +1419,40 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1227
1419
  task.description,
1228
1420
  commitMsg,
1229
1421
  );
1422
+ const producedChanges = Boolean(commitResult?.branchName || commitResult?.nestedPatches.length);
1230
1423
  return {
1231
- ...result,
1424
+ ...resultWithForkContext,
1232
1425
  branchName: commitResult?.branchName,
1233
1426
  nestedPatches: commitResult?.nestedPatches,
1427
+ producedChanges,
1234
1428
  };
1235
1429
  } catch (mergeErr) {
1236
1430
  // Agent succeeded but branch commit failed — clean up stale branch
1237
1431
  const branchName = `gjc/task/${task.id}`;
1238
1432
  await git.branch.tryDelete(repoRoot, branchName);
1239
1433
  const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
1240
- return { ...result, error: `Merge failed: ${msg}` };
1434
+ return { ...resultWithForkContext, error: `Merge failed: ${msg}` };
1241
1435
  }
1242
1436
  }
1243
- if (result.exitCode === 0) {
1437
+ if (resultWithForkContext.exitCode === 0) {
1244
1438
  try {
1245
1439
  const delta = await captureDeltaPatch(isolationDir, taskBaseline);
1246
- const patchPath = path.join(effectiveArtifactsDir, `${task.id}.patch`);
1440
+ const artifactId = validateAllocatedTaskId(task.id);
1441
+ const patchPath = path.join(effectiveArtifactsDir, `${artifactId}.patch`);
1247
1442
  await Bun.write(patchPath, delta.rootPatch);
1443
+ const producedChanges = Boolean(delta.rootPatch.trim() || delta.nestedPatches.length);
1248
1444
  return {
1249
- ...result,
1445
+ ...resultWithForkContext,
1250
1446
  patchPath,
1251
1447
  nestedPatches: delta.nestedPatches,
1448
+ producedChanges,
1252
1449
  };
1253
1450
  } catch (patchErr) {
1254
1451
  const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
1255
- return { ...result, error: `Patch capture failed: ${msg}` };
1452
+ return { ...resultWithForkContext, error: `Patch capture failed: ${msg}` };
1256
1453
  }
1257
1454
  }
1258
- return result;
1455
+ return resultWithForkContext;
1259
1456
  } catch (err) {
1260
1457
  const message = err instanceof Error ? err.message : String(err);
1261
1458
  const assignment = task.assignment.trim();
@@ -1274,6 +1471,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1274
1471
  durationMs: Date.now() - taskStart,
1275
1472
  tokens: 0,
1276
1473
  modelOverride,
1474
+ forkContext,
1277
1475
  error: message,
1278
1476
  };
1279
1477
  } finally {
@@ -1318,6 +1516,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1318
1516
  abortReason: "Cancelled before start",
1319
1517
  };
1320
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
+ );
1321
1530
 
1322
1531
  // Aggregate usage from executor results (already accumulated incrementally)
1323
1532
  const aggregatedUsage = createUsageTotals();
@@ -1329,13 +1538,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1329
1538
  }
1330
1539
  }
1331
1540
 
1332
- // Collect output paths (artifacts already written by executor in real-time)
1333
- const outputPaths: string[] = [];
1334
1541
  const patchPaths: string[] = [];
1335
1542
  for (const result of results) {
1336
- if (result.outputPath) {
1337
- outputPaths.push(result.outputPath);
1338
- }
1339
1543
  if (result.patchPath) {
1340
1544
  patchPaths.push(result.patchPath);
1341
1545
  }
@@ -1431,7 +1635,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1431
1635
  "<system-notification>Patches were not applied and must be handled manually.</system-notification>";
1432
1636
  const patchList =
1433
1637
  patchPaths.length > 0
1434
- ? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
1638
+ ? `\n\nPatch artifacts: ${patchPaths.length} preserved for internal merge recovery.`
1435
1639
  : "";
1436
1640
  mergeSummary = `\n\n${notification}${patchList}`;
1437
1641
  }
@@ -1487,41 +1691,25 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1487
1691
  const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
1488
1692
  const totalDuration = Date.now() - startTime;
1489
1693
 
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
- }
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;
1509
1698
  return {
1510
1699
  agent: r.agent,
1511
1700
  status,
1512
1701
  id: r.id,
1513
- preview,
1514
- truncated,
1515
- meta: r.outputMeta
1702
+ synopsis: r.preview,
1703
+ outputUri: r.outputRef?.uri,
1704
+ meta: r.outputRef
1516
1705
  ? {
1517
- lineCount: r.outputMeta.lineCount,
1518
- charSize: formatBytes(r.outputMeta.charCount),
1706
+ lineCount: r.outputRef.lineCount,
1707
+ charSize: formatBytes(r.outputRef.sizeBytes),
1519
1708
  }
1520
1709
  : undefined,
1521
1710
  };
1522
1711
  });
1523
1712
 
1524
- const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
1525
1713
  const summary = prompt.render(taskSummaryTemplate, {
1526
1714
  successCount,
1527
1715
  totalCount: results.length,
@@ -1529,7 +1717,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1529
1717
  hasCancelledNote: aborted && cancelledCount > 0,
1530
1718
  duration: formatDuration(totalDuration),
1531
1719
  summaries,
1532
- outputIds,
1533
1720
  agentName,
1534
1721
  mergeSummary,
1535
1722
  });
@@ -1541,15 +1728,18 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1541
1728
  await fs.rm(tempArtifactsDir, { recursive: true, force: true });
1542
1729
  }
1543
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");
1544
1740
  return {
1545
1741
  content: [{ type: "text", text: summary }],
1546
- details: {
1547
- projectAgentsDir,
1548
- results: results,
1549
- totalDurationMs: totalDuration,
1550
- usage: hasAggregatedUsage ? aggregatedUsage : undefined,
1551
- outputPaths,
1552
- },
1742
+ details,
1553
1743
  };
1554
1744
  } catch (err) {
1555
1745
  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
  /**