ultimate-pi 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
  2. package/.agents/skills/harness-decisions/SKILL.md +1 -2
  3. package/.agents/skills/harness-governor/SKILL.md +6 -5
  4. package/.pi/PACKAGING.md +4 -4
  5. package/.pi/SYSTEM.md +54 -120
  6. package/.pi/agents/harness/incident-recorder.md +0 -1
  7. package/.pi/agents/harness/planning/decompose.md +0 -2
  8. package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
  9. package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
  10. package/.pi/agents/harness/planning/hypothesis.md +0 -2
  11. package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
  12. package/.pi/agents/harness/planning/plan-adversary.md +0 -2
  13. package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
  14. package/.pi/agents/harness/planning/planning-context.md +0 -2
  15. package/.pi/agents/harness/planning/review-integrator.md +0 -2
  16. package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
  17. package/.pi/agents/harness/planning/stack-researcher.md +0 -2
  18. package/.pi/agents/harness/reviewing/adversary.md +0 -2
  19. package/.pi/agents/harness/reviewing/evaluator.md +0 -2
  20. package/.pi/agents/harness/reviewing/tie-breaker.md +0 -2
  21. package/.pi/agents/harness/running/executor.md +0 -2
  22. package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
  23. package/.pi/agents/harness/sentrux-steward.md +0 -2
  24. package/.pi/agents/harness/trace-librarian.md +0 -1
  25. package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
  26. package/.pi/extensions/agt-kill-switch.ts +57 -0
  27. package/.pi/extensions/agt-prompt-guard.ts +32 -0
  28. package/.pi/extensions/custom-footer.ts +46 -145
  29. package/.pi/extensions/custom-header.ts +1 -1
  30. package/.pi/extensions/custom-system-prompt.ts +1 -1
  31. package/.pi/extensions/debate-orchestrator.ts +6 -6
  32. package/.pi/extensions/harness-ask-user.ts +7 -7
  33. package/.pi/extensions/harness-debate-tools.ts +26 -42
  34. package/.pi/extensions/harness-lens.ts +94 -0
  35. package/.pi/extensions/harness-plan-approval.ts +11 -11
  36. package/.pi/extensions/harness-run-context.ts +1070 -876
  37. package/.pi/extensions/harness-subagent-governance.ts +8 -0
  38. package/.pi/extensions/harness-subagent-submit.ts +34 -163
  39. package/.pi/extensions/harness-subagents.ts +3 -3
  40. package/.pi/extensions/harness-telemetry.ts +2 -2
  41. package/.pi/extensions/harness-web-tools.ts +2 -2
  42. package/.pi/extensions/policy-gate.ts +25 -5
  43. package/.pi/extensions/sentrux-rules-sync.ts +1 -1
  44. package/.pi/extensions/subagent-governance.ts +92 -0
  45. package/.pi/extensions/trace-recorder.ts +1 -1
  46. package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
  47. package/.pi/harness/README.md +6 -2
  48. package/.pi/harness/agents.manifest.json +22 -25
  49. package/.pi/harness/agents.policy.yaml +275 -0
  50. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
  51. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
  52. package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
  53. package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
  54. package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
  55. package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
  56. package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
  57. package/.pi/harness/docs/adrs/README.md +5 -0
  58. package/.pi/harness/evolution/README.md +1 -2
  59. package/.pi/harness/examples/agents.policy.project.yaml +19 -0
  60. package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
  61. package/.pi/harness/policies/bash-denylists.yaml +5 -0
  62. package/.pi/harness/policies/defaults.yaml +51 -0
  63. package/.pi/harness/policies/orchestrator.yaml +18 -0
  64. package/.pi/harness/policies/phases.yaml +10 -0
  65. package/.pi/harness/policies/roles.yaml +5 -0
  66. package/.pi/harness/policies/web-guard.yaml +5 -0
  67. package/.pi/harness/policies/workflow-sequences.yaml +9 -0
  68. package/.pi/harness/sentrux/architecture.manifest.json +26 -4
  69. package/.pi/harness/specs/observation.schema.json +2 -1
  70. package/.pi/lib/agents-policy.d.mts +70 -0
  71. package/.pi/lib/agents-policy.mjs +325 -0
  72. package/.pi/lib/agents-policy.ts +19 -0
  73. package/.pi/lib/agt/audit-run-sink.ts +52 -0
  74. package/.pi/lib/agt/build-evaluation-context.ts +285 -0
  75. package/.pi/lib/agt/config.ts +28 -0
  76. package/.pi/lib/agt/delegation.ts +69 -0
  77. package/.pi/lib/agt/evaluate-policy.ts +56 -0
  78. package/.pi/lib/agt/identity-registry.ts +41 -0
  79. package/.pi/lib/agt/index.ts +55 -0
  80. package/.pi/lib/agt/kill-switch-state.ts +11 -0
  81. package/.pi/lib/agt/legacy-evaluate.ts +101 -0
  82. package/.pi/lib/agt/policy-engine.ts +154 -0
  83. package/.pi/lib/agt/rings.ts +21 -0
  84. package/.pi/lib/agt/sre-hooks.ts +45 -0
  85. package/.pi/lib/agt/trust-run-store.ts +26 -0
  86. package/.pi/lib/agt/workflow-history.ts +29 -0
  87. package/.pi/lib/agt-governance-active.ts +14 -0
  88. package/.pi/lib/agt-tool-guard.ts +78 -0
  89. package/.pi/lib/ask-user/dialog.ts +314 -0
  90. package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
  91. package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
  92. package/.pi/{extensions/lib → lib}/extension-load-guard.ts +13 -2
  93. package/.pi/lib/harness-agt-tool-guard.ts +5 -0
  94. package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +1 -1
  95. package/.pi/lib/harness-debate-core-deps.ts +14 -0
  96. package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
  97. package/.pi/lib/harness-lens/.gitattributes +1 -0
  98. package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
  99. package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
  100. package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
  101. package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
  102. package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
  103. package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
  104. package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
  105. package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
  106. package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
  107. package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
  108. package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
  109. package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
  110. package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
  111. package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
  112. package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
  113. package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
  114. package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
  115. package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
  116. package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
  117. package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
  118. package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
  119. package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
  120. package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
  121. package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
  122. package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
  123. package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
  124. package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
  125. package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
  126. package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
  127. package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
  128. package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
  129. package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
  130. package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
  131. package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
  132. package/.pi/lib/harness-lens/clients/types.ts +59 -0
  133. package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
  134. package/.pi/lib/harness-lens/index.ts +532 -0
  135. package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
  136. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
  137. package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
  138. package/.pi/lib/harness-run-context-responses.ts +9 -0
  139. package/.pi/lib/harness-run-context.ts +0 -2
  140. package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +1 -0
  141. package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +1 -1
  142. package/.pi/lib/harness-subagent-auth.ts +51 -0
  143. package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +10 -7
  144. package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
  145. package/.pi/lib/harness-subagent-submit-register.ts +163 -0
  146. package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -37
  147. package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
  148. package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
  149. package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
  150. package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
  151. package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
  152. package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
  153. package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
  154. package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
  155. package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
  156. package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
  157. package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
  158. package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
  159. package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
  160. package/.pi/prompts/harness-plan.md +1 -1
  161. package/.pi/prompts/harness-setup.md +37 -64
  162. package/.pi/scripts/README.md +2 -5
  163. package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
  164. package/.pi/scripts/harness-agents-manifest.mjs +60 -3
  165. package/.pi/scripts/harness-agt-doctor.ts +36 -0
  166. package/.pi/scripts/harness-cli-verify.sh +9 -2
  167. package/.pi/scripts/harness-verify.mjs +113 -39
  168. package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
  169. package/.pi/scripts/validate-plan-dag.mjs +65 -74
  170. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
  171. package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
  172. package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
  173. package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
  174. package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
  175. package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
  176. package/.pi/skills/architecture/layered/SKILL.md +68 -0
  177. package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
  178. package/.pi/skills/architecture/microservices/SKILL.md +64 -0
  179. package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
  180. package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
  181. package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
  182. package/.pi/skills/architecture/service-based/SKILL.md +64 -0
  183. package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
  184. package/.pi/skills/architecture/space-based/SKILL.md +60 -0
  185. package/.pi/skills/ast-grep/SKILL.md +40 -321
  186. package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
  187. package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
  188. package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
  189. package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
  190. package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
  191. package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
  192. package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
  193. package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
  194. package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
  195. package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
  196. package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
  197. package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
  198. package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
  199. package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
  200. package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
  201. package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
  202. package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
  203. package/.pi/skills/lsp-navigation/SKILL.md +89 -0
  204. package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
  205. package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
  206. package/.pi/skills/quality/security-review/SKILL.md +34 -0
  207. package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
  208. package/.pi/skills/quality/testability-design/SKILL.md +33 -0
  209. package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
  210. package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
  211. package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
  212. package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
  213. package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
  214. package/.sentrux/rules.toml +20 -4
  215. package/AGENTS.md +5 -0
  216. package/CHANGELOG.md +14 -0
  217. package/README.md +3 -12
  218. package/THIRD_PARTY_NOTICES.md +12 -21
  219. package/package.json +15 -7
  220. package/vendor/pi-subagents/src/agents.ts +45 -1
  221. package/vendor/pi-subagents/src/subagents.ts +866 -811
  222. package/vendor/pi-vcc/src/core/brief.ts +68 -99
  223. package/vendor/pi-vcc/src/core/settings.ts +2 -2
  224. package/.agents/skills/caveman/SKILL.md +0 -67
  225. package/.pi/agents/harness/meta-optimizer.md +0 -36
  226. package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
  227. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -207
  228. package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
  229. package/.pi/extensions/pi-model-router-harness.ts +0 -42
  230. package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
  231. package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
  232. package/.pi/model-router.example.json +0 -36
  233. package/.pi/prompts/harness-critic.md +0 -10
  234. package/.pi/prompts/harness-eval.md +0 -10
  235. package/.pi/prompts/harness-router-tune.md +0 -52
  236. package/.pi/scripts/harness-generate-model-router.mjs +0 -327
  237. package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
  238. package/.pi/scripts/harness-sync-model-router.mjs +0 -97
  239. package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
  240. package/vendor/pi-model-router/.prettierignore +0 -4
  241. package/vendor/pi-model-router/.prettierrc +0 -5
  242. package/vendor/pi-model-router/AGENTS.md +0 -39
  243. package/vendor/pi-model-router/LICENSE +0 -21
  244. package/vendor/pi-model-router/README.md +0 -99
  245. package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
  246. package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
  247. package/vendor/pi-model-router/extensions/commands.ts +0 -720
  248. package/vendor/pi-model-router/extensions/config.ts +0 -348
  249. package/vendor/pi-model-router/extensions/constants.ts +0 -1
  250. package/vendor/pi-model-router/extensions/index.ts +0 -478
  251. package/vendor/pi-model-router/extensions/provider.ts +0 -580
  252. package/vendor/pi-model-router/extensions/routing.ts +0 -564
  253. package/vendor/pi-model-router/extensions/state.ts +0 -52
  254. package/vendor/pi-model-router/extensions/types.ts +0 -95
  255. package/vendor/pi-model-router/extensions/ui.ts +0 -144
  256. package/vendor/pi-model-router/model-router.example.json +0 -48
  257. package/vendor/pi-model-router/package.json +0 -48
  258. package/vendor/pi-model-router/tsconfig.json +0 -16
  259. /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
  260. /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
  261. /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
  262. /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
  263. /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
  264. /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
  265. /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
  266. /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
  267. /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
  268. /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
  269. /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
  270. /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
  271. /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
  272. /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
  273. /package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +0 -0
  274. /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
  275. /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
  276. /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
  277. /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
  278. /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
  279. /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
  280. /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
  281. /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
  282. /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
  283. /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
  284. /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
@@ -42,7 +42,9 @@ export interface SpawnAuthForward {
42
42
 
43
43
  export interface HarnessSubagentsOptions {
44
44
  packageRoot?: string;
45
- /** Absolute path to harness-subagent-submit.ts for subprocess-only extension loading (Option A). */
45
+ /** Absolute path to subprocess governance extension (AGT + harness submit tools). */
46
+ subprocessGovernanceExtensionPath?: string;
47
+ /** @deprecated Use subprocessGovernanceExtensionPath */
46
48
  harnessSubprocessExtensionPath?: string;
47
49
  /** Extra env vars per subprocess (e.g. HARNESS_RUN_ID, HARNESS_RUN_DIR). */
48
50
  resolveSubprocessEnv?: (
@@ -313,7 +315,7 @@ function getDisplayItems(messages: Message[]): DisplayItem[] {
313
315
  for (const msg of messages) {
314
316
  if (msg.role === "assistant") {
315
317
  for (const part of msg.content) {
316
- if (part.type === "text") items.push({ type: "text", text: part.text });
318
+ if (part.type === "text") items.push({ type: "text" as const, text: part.text });
317
319
  else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
318
320
  }
319
321
  }
@@ -408,6 +410,204 @@ function buildSpawnEnv(
408
410
  return env;
409
411
  }
410
412
 
413
+ function unknownAgentResult(
414
+ agents: AgentConfig[],
415
+ agentName: string,
416
+ task: string,
417
+ step: number | undefined,
418
+ ): SingleResult {
419
+ const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
420
+ return {
421
+ agent: agentName,
422
+ agentSource: "unknown",
423
+ task,
424
+ exitCode: 1,
425
+ messages: [],
426
+ stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
427
+ usage: {
428
+ input: 0,
429
+ output: 0,
430
+ cacheRead: 0,
431
+ cacheWrite: 0,
432
+ cost: 0,
433
+ contextTokens: 0,
434
+ turns: 0,
435
+ },
436
+ step,
437
+ finalOutput: "",
438
+ };
439
+ }
440
+
441
+ function buildAgentArgs(
442
+ agent: AgentConfig,
443
+ spawnAuth: SpawnAuthForward | undefined,
444
+ subagentsOptions: HarnessSubagentsOptions | undefined,
445
+ ): string[] {
446
+ const args: string[] = ["--mode", "json", "-p", "--no-session"];
447
+ if (agent.model) args.push("--model", agent.model);
448
+ else if (spawnAuth) args.push("--model", spawnAuth.modelRef);
449
+ if (spawnAuth?.apiKey) args.push("--api-key", spawnAuth.apiKey);
450
+ if (agent.thinking) args.push("--thinking", agent.thinking);
451
+
452
+ const governanceExt =
453
+ agent.extensionsOff &&
454
+ (subagentsOptions?.subprocessGovernanceExtensionPath ??
455
+ subagentsOptions?.harnessSubprocessExtensionPath);
456
+ if (agent.extensionsOff) {
457
+ args.push("--no-extensions");
458
+ if (governanceExt) args.push("-e", governanceExt);
459
+ if (agent.skillsOff) args.push("--no-skills");
460
+ }
461
+ if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
462
+ else if (agent.extensionsOff) args.push("--no-tools");
463
+ return args;
464
+ }
465
+
466
+ function appendSubagentEvent(
467
+ currentResult: SingleResult,
468
+ event: any,
469
+ emitUpdate: () => void,
470
+ ): void {
471
+ if (event.type === "message_end" && event.message) {
472
+ const msg = event.message as Message;
473
+ currentResult.messages.push(msg);
474
+ if (msg.role === "assistant") {
475
+ currentResult.usage.turns += 1;
476
+ const usage = msg.usage;
477
+ if (usage) {
478
+ currentResult.usage.input += usage.input || 0;
479
+ currentResult.usage.output += usage.output || 0;
480
+ currentResult.usage.cacheRead += usage.cacheRead || 0;
481
+ currentResult.usage.cacheWrite += usage.cacheWrite || 0;
482
+ currentResult.usage.cost += usage.cost?.total || 0;
483
+ currentResult.usage.contextTokens = usage.totalTokens || 0;
484
+ }
485
+ if (!currentResult.model && msg.model) currentResult.model = msg.model;
486
+ if (msg.stopReason) currentResult.stopReason = msg.stopReason;
487
+ if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
488
+ }
489
+ emitUpdate();
490
+ return;
491
+ }
492
+ if (event.type === "tool_result_end" && event.message) {
493
+ currentResult.messages.push(event.message as Message);
494
+ emitUpdate();
495
+ }
496
+ }
497
+
498
+ function parseSubagentLine(
499
+ line: string,
500
+ currentResult: SingleResult,
501
+ emitUpdate: () => void,
502
+ ): void {
503
+ if (!line.trim()) return;
504
+ let event: any;
505
+ try {
506
+ event = JSON.parse(line);
507
+ } catch {
508
+ return;
509
+ }
510
+ appendSubagentEvent(currentResult, event, emitUpdate);
511
+ }
512
+
513
+ function cleanupTempPromptFiles(
514
+ tmpPromptPath: string | null,
515
+ tmpPromptDir: string | null,
516
+ ): void {
517
+ if (tmpPromptPath) {
518
+ try {
519
+ fs.unlinkSync(tmpPromptPath);
520
+ } catch {
521
+ /* ignore */
522
+ }
523
+ }
524
+ if (tmpPromptDir) {
525
+ try {
526
+ fs.rmdirSync(tmpPromptDir);
527
+ } catch {
528
+ /* ignore */
529
+ }
530
+ }
531
+ }
532
+
533
+ async function runSubagentProcess(
534
+ invocation: { command: string; args: string[] },
535
+ runtime: {
536
+ cwd: string;
537
+ env: NodeJS.ProcessEnv;
538
+ timeoutMs?: number;
539
+ signal?: AbortSignal;
540
+ },
541
+ currentResult: SingleResult,
542
+ emitUpdate: () => void,
543
+ ): Promise<{ exitCode: number; wasAborted: boolean; timedOut: boolean }> {
544
+ let wasAborted = false;
545
+ let timedOut = false;
546
+ const exitCode = await new Promise<number>((resolve) => {
547
+ let settled = false;
548
+ let timeout: ReturnType<typeof setTimeout> | undefined;
549
+ const finish = (code: number) => {
550
+ if (settled) return;
551
+ settled = true;
552
+ if (timeout) clearTimeout(timeout);
553
+ resolve(code);
554
+ };
555
+ const proc = spawn(invocation.command, invocation.args, {
556
+ cwd: runtime.cwd,
557
+ env: runtime.env,
558
+ detached: process.platform !== "win32",
559
+ shell: false,
560
+ stdio: ["ignore", "pipe", "pipe"],
561
+ });
562
+ let buffer = "";
563
+ if (runtime.timeoutMs != null) {
564
+ timeout = setTimeout(() => {
565
+ timedOut = true;
566
+ currentResult.timedOut = true;
567
+ currentResult.stopReason = "timeout";
568
+ currentResult.errorMessage = `Subagent timed out after ${runtime.timeoutMs}ms`;
569
+ currentResult.stderr += `${currentResult.stderr ? "\n" : ""}Subagent timed out after ${runtime.timeoutMs}ms.`;
570
+ emitUpdate();
571
+ terminateProcess(proc);
572
+ }, runtime.timeoutMs);
573
+ timeout.unref();
574
+ }
575
+
576
+ proc.stdout.on("data", (data) => {
577
+ buffer += data.toString();
578
+ const lines = buffer.split("\n");
579
+ buffer = lines.pop() || "";
580
+ for (const line of lines) parseSubagentLine(line, currentResult, emitUpdate);
581
+ });
582
+
583
+ proc.stderr.on("data", (data) => {
584
+ currentResult.stderr += data.toString();
585
+ });
586
+
587
+ proc.on("close", (code) => {
588
+ if (buffer.trim()) parseSubagentLine(buffer, currentResult, emitUpdate);
589
+ finish(timedOut ? 124 : (code ?? 0));
590
+ });
591
+
592
+ proc.on("error", (error) => {
593
+ currentResult.errorMessage = error.message;
594
+ currentResult.stderr += `${currentResult.stderr ? "\n" : ""}${error.message}`;
595
+ finish(1);
596
+ });
597
+
598
+ if (!runtime.signal) return;
599
+ const killProc = () => {
600
+ wasAborted = true;
601
+ currentResult.stopReason = "aborted";
602
+ currentResult.errorMessage = "Subagent was aborted";
603
+ terminateProcess(proc);
604
+ };
605
+ if (runtime.signal.aborted) killProc();
606
+ else runtime.signal.addEventListener("abort", killProc, { once: true });
607
+ });
608
+ return { exitCode, wasAborted, timedOut };
609
+ }
610
+
411
611
  async function runSingleAgent(
412
612
  defaultCwd: string,
413
613
  agents: AgentConfig[],
@@ -424,43 +624,9 @@ async function runSingleAgent(
424
624
  subagentsOptions?: HarnessSubagentsOptions,
425
625
  ): Promise<SingleResult> {
426
626
  const agent = agents.find((a) => a.name === agentName);
627
+ if (!agent) return unknownAgentResult(agents, agentName, task, step);
427
628
 
428
- if (!agent) {
429
- const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
430
- return {
431
- agent: agentName,
432
- agentSource: "unknown",
433
- task,
434
- exitCode: 1,
435
- messages: [],
436
- stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
437
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
438
- step,
439
- finalOutput: "",
440
- };
441
- }
442
-
443
- const args: string[] = ["--mode", "json", "-p", "--no-session"];
444
- if (agent.model) args.push("--model", agent.model);
445
- else if (spawnAuth) args.push("--model", spawnAuth.modelRef);
446
- if (spawnAuth?.apiKey) args.push("--api-key", spawnAuth.apiKey);
447
- if (agent.thinking) args.push("--thinking", agent.thinking);
448
- const harnessExt =
449
- agent.extensionsOff &&
450
- agent.name.startsWith("harness/") &&
451
- subagentsOptions?.harnessSubprocessExtensionPath;
452
- if (agent.extensionsOff) {
453
- args.push("--no-extensions");
454
- if (harnessExt) {
455
- args.push("-e", harnessExt);
456
- }
457
- if (agent.skillsOff) args.push("--no-skills");
458
- }
459
- if (agent.tools && agent.tools.length > 0) {
460
- args.push("--tools", agent.tools.join(","));
461
- } else if (agent.extensionsOff) {
462
- args.push("--no-tools");
463
- }
629
+ const args = buildAgentArgs(agent, spawnAuth, subagentsOptions);
464
630
  const extraEnv = subagentsOptions?.resolveSubprocessEnv?.(task, agent);
465
631
  const spawnEnv = buildSpawnEnv(packageRoot, {
466
632
  ...extraEnv,
@@ -469,7 +635,6 @@ async function runSingleAgent(
469
635
 
470
636
  let tmpPromptDir: string | null = null;
471
637
  let tmpPromptPath: string | null = null;
472
-
473
638
  const currentResult: SingleResult = {
474
639
  agent: agentName,
475
640
  agentSource: agent.source,
@@ -477,20 +642,26 @@ async function runSingleAgent(
477
642
  exitCode: 0,
478
643
  messages: [],
479
644
  stderr: "",
480
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
645
+ usage: {
646
+ input: 0,
647
+ output: 0,
648
+ cacheRead: 0,
649
+ cacheWrite: 0,
650
+ cost: 0,
651
+ contextTokens: 0,
652
+ turns: 0,
653
+ },
481
654
  model: agent.model,
482
655
  step,
483
656
  timeoutMs,
484
657
  };
485
-
486
658
  const emitUpdate = () => {
487
659
  currentResult.finalOutput = getFinalOutput(currentResult.messages);
488
- if (onUpdate) {
489
- onUpdate({
490
- content: [{ type: "text", text: currentResult.finalOutput || "(running...)" }],
491
- details: makeDetails([currentResult]),
492
- });
493
- }
660
+ if (!onUpdate) return;
661
+ onUpdate({
662
+ content: [{ type: "text" as const, text: currentResult.finalOutput || "(running...)" }],
663
+ details: makeDetails([currentResult]),
664
+ });
494
665
  };
495
666
 
496
667
  try {
@@ -500,130 +671,27 @@ async function runSingleAgent(
500
671
  tmpPromptPath = tmp.filePath;
501
672
  args.push("--append-system-prompt", tmpPromptPath);
502
673
  }
503
-
504
674
  args.push(`Task: ${task}`);
505
- let wasAborted = false;
506
- let timedOut = false;
507
-
508
- const exitCode = await new Promise<number>((resolve) => {
509
- const invocation = getPiInvocation(args);
510
- let settled = false;
511
- let timeout: ReturnType<typeof setTimeout> | undefined;
512
- const finish = (code: number) => {
513
- if (settled) return;
514
- settled = true;
515
- if (timeout) clearTimeout(timeout);
516
- resolve(code);
517
- };
518
- const proc = spawn(invocation.command, invocation.args, {
675
+ const invocation = getPiInvocation(args);
676
+ const runtimeResult = await runSubagentProcess(
677
+ invocation,
678
+ {
519
679
  cwd: cwd ?? defaultCwd,
520
680
  env: spawnEnv,
521
- detached: process.platform !== "win32",
522
- shell: false,
523
- stdio: ["ignore", "pipe", "pipe"],
524
- });
525
- let buffer = "";
526
- if (timeoutMs != null) {
527
- timeout = setTimeout(() => {
528
- timedOut = true;
529
- currentResult.timedOut = true;
530
- currentResult.stopReason = "timeout";
531
- currentResult.errorMessage = `Subagent timed out after ${timeoutMs}ms`;
532
- currentResult.stderr += `${currentResult.stderr ? "\n" : ""}Subagent timed out after ${timeoutMs}ms.`;
533
- emitUpdate();
534
- terminateProcess(proc);
535
- }, timeoutMs);
536
- timeout.unref();
537
- }
538
-
539
- const processLine = (line: string) => {
540
- if (!line.trim()) return;
541
- let event: any;
542
- try {
543
- event = JSON.parse(line);
544
- } catch {
545
- return;
546
- }
547
-
548
- if (event.type === "message_end" && event.message) {
549
- const msg = event.message as Message;
550
- currentResult.messages.push(msg);
551
-
552
- if (msg.role === "assistant") {
553
- currentResult.usage.turns++;
554
- const usage = msg.usage;
555
- if (usage) {
556
- currentResult.usage.input += usage.input || 0;
557
- currentResult.usage.output += usage.output || 0;
558
- currentResult.usage.cacheRead += usage.cacheRead || 0;
559
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
560
- currentResult.usage.cost += usage.cost?.total || 0;
561
- currentResult.usage.contextTokens = usage.totalTokens || 0;
562
- }
563
- if (!currentResult.model && msg.model) currentResult.model = msg.model;
564
- if (msg.stopReason) currentResult.stopReason = msg.stopReason;
565
- if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
566
- }
567
- emitUpdate();
568
- }
569
-
570
- if (event.type === "tool_result_end" && event.message) {
571
- currentResult.messages.push(event.message as Message);
572
- emitUpdate();
573
- }
574
- };
575
-
576
- proc.stdout.on("data", (data) => {
577
- buffer += data.toString();
578
- const lines = buffer.split("\n");
579
- buffer = lines.pop() || "";
580
- for (const line of lines) processLine(line);
581
- });
582
-
583
- proc.stderr.on("data", (data) => {
584
- currentResult.stderr += data.toString();
585
- });
586
-
587
- proc.on("close", (code) => {
588
- if (buffer.trim()) processLine(buffer);
589
- finish(timedOut ? 124 : (code ?? 0));
590
- });
591
-
592
- proc.on("error", (error) => {
593
- currentResult.errorMessage = error.message;
594
- currentResult.stderr += `${currentResult.stderr ? "\n" : ""}${error.message}`;
595
- finish(1);
596
- });
597
-
598
- if (signal) {
599
- const killProc = () => {
600
- wasAborted = true;
601
- currentResult.stopReason = "aborted";
602
- currentResult.errorMessage = "Subagent was aborted";
603
- terminateProcess(proc);
604
- };
605
- if (signal.aborted) killProc();
606
- else signal.addEventListener("abort", killProc, { once: true });
607
- }
608
- });
609
-
610
- currentResult.exitCode = exitCode;
681
+ timeoutMs,
682
+ signal,
683
+ },
684
+ currentResult,
685
+ emitUpdate,
686
+ );
687
+ currentResult.exitCode = runtimeResult.exitCode;
611
688
  currentResult.finalOutput = getFinalOutput(currentResult.messages);
612
- if (wasAborted && !timedOut) throw new Error("Subagent was aborted");
689
+ if (runtimeResult.wasAborted && !runtimeResult.timedOut) {
690
+ throw new Error("Subagent was aborted");
691
+ }
613
692
  return currentResult;
614
693
  } finally {
615
- if (tmpPromptPath)
616
- try {
617
- fs.unlinkSync(tmpPromptPath);
618
- } catch {
619
- /* ignore */
620
- }
621
- if (tmpPromptDir)
622
- try {
623
- fs.rmdirSync(tmpPromptDir);
624
- } catch {
625
- /* ignore */
626
- }
694
+ cleanupTempPromptFiles(tmpPromptPath, tmpPromptDir);
627
695
  }
628
696
  }
629
697
 
@@ -696,6 +764,575 @@ function truncateSubagentDetails(
696
764
  };
697
765
  }
698
766
 
767
+ type SubagentToolParams = {
768
+ agent?: string;
769
+ task?: string;
770
+ tasks?: Array<{ agent: string; task: string; cwd?: string; timeoutMs?: number }>;
771
+ chain?: Array<{ agent: string; task: string; cwd?: string; timeoutMs?: number }>;
772
+ aggregator?: { agent: string; task: string; cwd?: string; timeoutMs?: number };
773
+ agentScope?: AgentScope;
774
+ confirmProjectAgents?: boolean;
775
+ cwd?: string;
776
+ timeoutMs?: number;
777
+ };
778
+
779
+ type SubagentExecuteContext = {
780
+ toolCallId: string;
781
+ params: SubagentToolParams;
782
+ signal: AbortSignal | undefined;
783
+ onUpdate: OnUpdateCallback | undefined;
784
+ ctx: ExtensionContext;
785
+ agents: AgentConfig[];
786
+ discovery: ReturnType<typeof discoverAgents>;
787
+ agentScope: AgentScope;
788
+ defaultTimeoutMs: number | undefined;
789
+ packageRoot?: string;
790
+ options: HarnessSubagentsOptions;
791
+ resolveSpawnAuth: (agentName: string) => Promise<SpawnAuthForward | undefined>;
792
+ makeDetails: (
793
+ mode: "single" | "parallel" | "chain",
794
+ ) => (results: SingleResult[], aggregator?: SingleResult) => SubagentDetails;
795
+ };
796
+
797
+ function collectHarnessAgents(params: SubagentToolParams): string[] {
798
+ const harnessAgents: string[] = [];
799
+ if (params.agent?.startsWith("harness/")) harnessAgents.push(params.agent);
800
+ for (const task of params.tasks ?? []) {
801
+ if (task.agent.startsWith("harness/")) harnessAgents.push(task.agent);
802
+ }
803
+ for (const step of params.chain ?? []) {
804
+ if (step.agent.startsWith("harness/")) harnessAgents.push(step.agent);
805
+ }
806
+ if (params.aggregator?.agent.startsWith("harness/")) {
807
+ harnessAgents.push(params.aggregator.agent);
808
+ }
809
+ return harnessAgents;
810
+ }
811
+
812
+ function modeInfo(params: SubagentToolParams): {
813
+ hasChain: boolean;
814
+ hasTasks: boolean;
815
+ hasSingle: boolean;
816
+ modeCount: number;
817
+ } {
818
+ const hasChain = (params.chain?.length ?? 0) > 0;
819
+ const hasTasks = (params.tasks?.length ?? 0) > 0;
820
+ const hasSingle = Boolean(params.agent && params.task);
821
+ return {
822
+ hasChain,
823
+ hasTasks,
824
+ hasSingle,
825
+ modeCount: Number(hasChain) + Number(hasTasks) + Number(hasSingle),
826
+ };
827
+ }
828
+
829
+ async function maybeConfirmProjectAgents(
830
+ execCtx: SubagentExecuteContext,
831
+ mode: "single" | "parallel" | "chain",
832
+ ): Promise<boolean> {
833
+ const { params, ctx, agents, discovery, agentScope } = execCtx;
834
+ if (!ctx.hasUI) return true;
835
+ if (agentScope !== "project" && agentScope !== "both") return true;
836
+ if (!params.confirmProjectAgents) return true;
837
+
838
+ const requested = new Set<string>();
839
+ if (params.agent) requested.add(params.agent);
840
+ if (params.aggregator) requested.add(params.aggregator.agent);
841
+ for (const task of params.tasks ?? []) requested.add(task.agent);
842
+ for (const step of params.chain ?? []) requested.add(step.agent);
843
+
844
+ const projectAgents = Array.from(requested)
845
+ .map((name) => agents.find((a) => a.name === name))
846
+ .filter((a): a is AgentConfig => a?.source === "project");
847
+ if (projectAgents.length === 0) return true;
848
+
849
+ const names = projectAgents.map((a) => a.name).join(", ");
850
+ const dir = discovery.projectAgentsDir ?? "(unknown)";
851
+ const ok = await ctx.ui.confirm(
852
+ "Run project-local agents?",
853
+ `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
854
+ );
855
+ if (ok) return true;
856
+
857
+ execCtx.onUpdate?.({
858
+ content: [{ type: "text" as const, text: "Canceled: project-local agents not approved." }],
859
+ details: execCtx.makeDetails(mode)([]),
860
+ });
861
+ return false;
862
+ }
863
+
864
+ async function executeChainMode(execCtx: SubagentExecuteContext) {
865
+ const { params, ctx, toolCallId, signal, defaultTimeoutMs, onUpdate } = execCtx;
866
+ const chain = params.chain ?? [];
867
+ const results: SingleResult[] = [];
868
+ let previousOutput = "";
869
+ const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, chain.length));
870
+ try {
871
+ for (let i = 0; i < chain.length; i++) {
872
+ const step = chain[i];
873
+ status.update(chainStatus(i + 1, chain.length, step.agent));
874
+ const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
875
+ const chainUpdate: OnUpdateCallback | undefined = onUpdate
876
+ ? (partial) => {
877
+ const current = partial.details?.results[0];
878
+ if (!current) return;
879
+ onUpdate({
880
+ content: partial.content,
881
+ details: execCtx.makeDetails("chain")([...results, current]),
882
+ });
883
+ }
884
+ : undefined;
885
+ const result = await runSingleAgent(
886
+ ctx.cwd,
887
+ execCtx.agents,
888
+ step.agent,
889
+ taskWithContext,
890
+ step.cwd,
891
+ i + 1,
892
+ signal,
893
+ step.timeoutMs ?? defaultTimeoutMs,
894
+ chainUpdate,
895
+ execCtx.makeDetails("chain"),
896
+ execCtx.packageRoot,
897
+ await execCtx.resolveSpawnAuth(step.agent),
898
+ execCtx.options,
899
+ );
900
+ results.push(result);
901
+ const errored =
902
+ result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
903
+ if (errored) {
904
+ const errorMsg =
905
+ result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
906
+ return {
907
+ content: [{ type: "text" as const, text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
908
+ details: execCtx.makeDetails("chain")(results),
909
+ isError: true,
910
+ };
911
+ }
912
+ previousOutput = getResultFinalOutput(result);
913
+ }
914
+ return {
915
+ content: [{ type: "text" as const, text: getResultFinalOutput(results[results.length - 1]) || "(no output)" }],
916
+ details: execCtx.makeDetails("chain")(results),
917
+ };
918
+ } finally {
919
+ status.clear();
920
+ }
921
+ }
922
+
923
+ function makeParallelPlaceholder(task: { agent: string; task: string }): SingleResult {
924
+ return {
925
+ agent: task.agent,
926
+ agentSource: "unknown",
927
+ task: task.task,
928
+ exitCode: -1,
929
+ messages: [],
930
+ stderr: "",
931
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
932
+ finalOutput: "",
933
+ };
934
+ }
935
+
936
+ async function executeParallelMode(execCtx: SubagentExecuteContext) {
937
+ const { params, ctx, toolCallId, signal, defaultTimeoutMs, onUpdate } = execCtx;
938
+ const tasks = params.tasks ?? [];
939
+ if (tasks.length > MAX_PARALLEL_TASKS) {
940
+ return {
941
+ content: [{ type: "text" as const, text: `Too many parallel tasks (${tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` }],
942
+ details: execCtx.makeDetails("parallel")([]),
943
+ };
944
+ }
945
+
946
+ const status = startSubagentStatus(ctx, toolCallId, parallelStatus(0, tasks.length, tasks.length));
947
+ try {
948
+ const allResults = tasks.map(makeParallelPlaceholder);
949
+ let doneCount = 0;
950
+ let runningCount = tasks.length;
951
+ const emitParallelUpdate = () => {
952
+ status.update(parallelStatus(doneCount, allResults.length, runningCount));
953
+ if (!onUpdate) return;
954
+ onUpdate({
955
+ content: [{ type: "text" as const, text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...` }],
956
+ details: execCtx.makeDetails("parallel")([...allResults]),
957
+ });
958
+ };
959
+
960
+ const results = await mapWithConcurrencyLimit(tasks, MAX_CONCURRENCY, async (task, index) => {
961
+ const result = await runSingleAgent(
962
+ ctx.cwd,
963
+ execCtx.agents,
964
+ task.agent,
965
+ task.task,
966
+ task.cwd,
967
+ undefined,
968
+ signal,
969
+ task.timeoutMs ?? defaultTimeoutMs,
970
+ (partial) => {
971
+ const current = partial.details?.results[0];
972
+ if (!current) return;
973
+ allResults[index] = { ...current, exitCode: -1 };
974
+ emitParallelUpdate();
975
+ },
976
+ execCtx.makeDetails("parallel"),
977
+ execCtx.packageRoot,
978
+ await execCtx.resolveSpawnAuth(task.agent),
979
+ execCtx.options,
980
+ );
981
+ allResults[index] = result;
982
+ doneCount += 1;
983
+ runningCount -= 1;
984
+ emitParallelUpdate();
985
+ return result;
986
+ });
987
+
988
+ let aggregatorResult: SingleResult | undefined;
989
+ if (params.aggregator) {
990
+ const aggregator = params.aggregator;
991
+ status.update(fanInStatus(aggregator.agent));
992
+ const fanInContext = buildFanInContext(results);
993
+ const aggregatorTask = aggregator.task.includes("{previous}")
994
+ ? aggregator.task.replace(/\{previous\}/g, fanInContext)
995
+ : `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`;
996
+ aggregatorResult = await runSingleAgent(
997
+ ctx.cwd,
998
+ execCtx.agents,
999
+ aggregator.agent,
1000
+ aggregatorTask,
1001
+ aggregator.cwd,
1002
+ undefined,
1003
+ signal,
1004
+ aggregator.timeoutMs ?? defaultTimeoutMs,
1005
+ (partial) => {
1006
+ status.update(fanInStatus(aggregator.agent));
1007
+ const current = partial.details?.results[0];
1008
+ if (!onUpdate || !current) return;
1009
+ onUpdate({
1010
+ content: partial.content,
1011
+ details: execCtx.makeDetails("parallel")(results, current),
1012
+ });
1013
+ },
1014
+ execCtx.makeDetails("parallel"),
1015
+ execCtx.packageRoot,
1016
+ await execCtx.resolveSpawnAuth(aggregator.agent),
1017
+ execCtx.options,
1018
+ );
1019
+ }
1020
+
1021
+ const successCount = results.filter((r) => r.exitCode === 0).length;
1022
+ const summaries = results.map((r) => {
1023
+ const summary = getResultFinalOutput(r) || r.errorMessage || r.stderr.trim();
1024
+ const preview = summary.slice(0, 160) + (summary.length > 160 ? "..." : "");
1025
+ return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
1026
+ });
1027
+ const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
1028
+ const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
1029
+ return {
1030
+ content: [
1031
+ {
1032
+ type: "text" as const,
1033
+ text: aggregatorResult
1034
+ ? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
1035
+ : `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
1036
+ },
1037
+ ],
1038
+ details: execCtx.makeDetails("parallel")(results, aggregatorResult),
1039
+ isError: aggregatorResult
1040
+ ? aggregatorResult.exitCode !== 0 ||
1041
+ aggregatorResult.stopReason === "error" ||
1042
+ aggregatorResult.stopReason === "aborted"
1043
+ : undefined,
1044
+ };
1045
+ } finally {
1046
+ status.clear();
1047
+ }
1048
+ }
1049
+
1050
+ async function executeSingleMode(execCtx: SubagentExecuteContext) {
1051
+ const { params, ctx, toolCallId, signal } = execCtx;
1052
+ const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent || "..."));
1053
+ try {
1054
+ const result = await runSingleAgent(
1055
+ ctx.cwd,
1056
+ execCtx.agents,
1057
+ params.agent || "",
1058
+ params.task || "",
1059
+ params.cwd,
1060
+ undefined,
1061
+ signal,
1062
+ params.timeoutMs ?? execCtx.defaultTimeoutMs,
1063
+ execCtx.onUpdate,
1064
+ execCtx.makeDetails("single"),
1065
+ execCtx.packageRoot,
1066
+ await execCtx.resolveSpawnAuth(params.agent || ""),
1067
+ execCtx.options,
1068
+ );
1069
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
1070
+ if (isError) {
1071
+ const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
1072
+ return {
1073
+ content: [{ type: "text" as const, text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
1074
+ details: execCtx.makeDetails("single")([result]),
1075
+ isError: true,
1076
+ };
1077
+ }
1078
+ return {
1079
+ content: [{ type: "text" as const, text: getResultFinalOutput(result) || "(no output)" }],
1080
+ details: execCtx.makeDetails("single")([result]),
1081
+ };
1082
+ } finally {
1083
+ status.clear();
1084
+ }
1085
+ }
1086
+
1087
+ function aggregateUsage(results: SingleResult[]) {
1088
+ const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
1089
+ for (const r of results) {
1090
+ total.input += r.usage.input;
1091
+ total.output += r.usage.output;
1092
+ total.cacheRead += r.usage.cacheRead;
1093
+ total.cacheWrite += r.usage.cacheWrite;
1094
+ total.cost += r.usage.cost;
1095
+ total.turns += r.usage.turns;
1096
+ }
1097
+ return total;
1098
+ }
1099
+
1100
+ function createDisplayItemRenderer(theme: any, expanded: boolean) {
1101
+ return (items: DisplayItem[], limit?: number): string => {
1102
+ const toShow = limit ? items.slice(-limit) : items;
1103
+ const skipped = limit && items.length > limit ? items.length - limit : 0;
1104
+ let text = "";
1105
+ if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
1106
+ for (const item of toShow) {
1107
+ if (item.type === "text") {
1108
+ const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
1109
+ text += `${theme.fg("toolOutput", preview)}\n`;
1110
+ continue;
1111
+ }
1112
+ text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
1113
+ }
1114
+ return text.trimEnd();
1115
+ };
1116
+ }
1117
+
1118
+ function renderSingleSubagentResult(details: SubagentDetails, expanded: boolean, theme: any, mdTheme: any, renderDisplayItems: (items: DisplayItem[], limit?: number) => string): any {
1119
+ const r = details.results[0];
1120
+ const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
1121
+ const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
1122
+ const displayItems = getDisplayItems(r.messages);
1123
+ const finalOutput = getResultFinalOutput(r);
1124
+ if (!expanded) {
1125
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
1126
+ if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
1127
+ if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
1128
+ else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
1129
+ else {
1130
+ text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
1131
+ if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1132
+ }
1133
+ const usageStr = formatUsageStats(r.usage, r.model);
1134
+ if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
1135
+ return new Text(text, 0, 0);
1136
+ }
1137
+
1138
+ const container = new Container();
1139
+ let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
1140
+ if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
1141
+ container.addChild(new Text(header, 0, 0));
1142
+ if (isError && r.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
1143
+ container.addChild(new Spacer(1));
1144
+ container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
1145
+ container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
1146
+ container.addChild(new Spacer(1));
1147
+ container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
1148
+ if (displayItems.length === 0 && !finalOutput) {
1149
+ container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
1150
+ } else {
1151
+ for (const item of displayItems) {
1152
+ if (item.type !== "toolCall") continue;
1153
+ container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
1154
+ }
1155
+ if (finalOutput) {
1156
+ container.addChild(new Spacer(1));
1157
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1158
+ }
1159
+ }
1160
+ const usageStr = formatUsageStats(r.usage, r.model);
1161
+ if (usageStr) {
1162
+ container.addChild(new Spacer(1));
1163
+ container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
1164
+ }
1165
+ return container;
1166
+ }
1167
+
1168
+ function renderChainSubagentResult(details: SubagentDetails, expanded: boolean, theme: any, mdTheme: any, renderDisplayItems: (items: DisplayItem[], limit?: number) => string): any {
1169
+ const successCount = details.results.filter((r) => r.exitCode === 0).length;
1170
+ const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
1171
+ if (!expanded) {
1172
+ let text = icon + " " + theme.fg("toolTitle", theme.bold("chain ")) + theme.fg("accent", `${successCount}/${details.results.length} steps`);
1173
+ for (const r of details.results) {
1174
+ const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1175
+ const displayItems = getDisplayItems(r.messages);
1176
+ text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
1177
+ text += displayItems.length === 0 ? `\n${theme.fg("muted", "(no output)")}` : `\n${renderDisplayItems(displayItems, 5)}`;
1178
+ }
1179
+ const usageStr = formatUsageStats(aggregateUsage(details.results));
1180
+ if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
1181
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1182
+ return new Text(text, 0, 0);
1183
+ }
1184
+ const container = new Container();
1185
+ container.addChild(new Text(icon + " " + theme.fg("toolTitle", theme.bold("chain ")) + theme.fg("accent", `${successCount}/${details.results.length} steps`), 0, 0));
1186
+ for (const r of details.results) {
1187
+ const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1188
+ const displayItems = getDisplayItems(r.messages);
1189
+ const finalOutput = getResultFinalOutput(r);
1190
+ container.addChild(new Spacer(1));
1191
+ container.addChild(new Text(`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0));
1192
+ container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
1193
+ for (const item of displayItems) {
1194
+ if (item.type !== "toolCall") continue;
1195
+ container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
1196
+ }
1197
+ if (finalOutput) {
1198
+ container.addChild(new Spacer(1));
1199
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1200
+ }
1201
+ const stepUsage = formatUsageStats(r.usage, r.model);
1202
+ if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
1203
+ }
1204
+ const usageStr = formatUsageStats(aggregateUsage(details.results));
1205
+ if (usageStr) {
1206
+ container.addChild(new Spacer(1));
1207
+ container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
1208
+ }
1209
+ return container;
1210
+ }
1211
+
1212
+ function renderParallelExpandedSubagentResult(details: SubagentDetails, theme: any, mdTheme: any): any {
1213
+ const container = new Container();
1214
+ const successCount = details.results.filter((r) => r.exitCode === 0).length;
1215
+ const aggregator = details.aggregator;
1216
+ const status = aggregator
1217
+ ? `${successCount}/${details.results.length} tasks + fan-in`
1218
+ : `${successCount}/${details.results.length} tasks`;
1219
+ container.addChild(new Text(`${theme.fg("success", "✓")} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`, 0, 0));
1220
+
1221
+ for (const r of details.results) {
1222
+ const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1223
+ const displayItems = getDisplayItems(r.messages);
1224
+ const finalOutput = getResultFinalOutput(r);
1225
+ container.addChild(new Spacer(1));
1226
+ container.addChild(new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0));
1227
+ container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
1228
+ for (const item of displayItems) {
1229
+ if (item.type !== "toolCall") continue;
1230
+ container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
1231
+ }
1232
+ if (finalOutput) {
1233
+ container.addChild(new Spacer(1));
1234
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1235
+ }
1236
+ const taskUsage = formatUsageStats(r.usage, r.model);
1237
+ if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
1238
+ }
1239
+
1240
+ if (aggregator) {
1241
+ const rIcon = aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1242
+ const displayItems = getDisplayItems(aggregator.messages);
1243
+ const finalOutput = getResultFinalOutput(aggregator);
1244
+ container.addChild(new Spacer(1));
1245
+ container.addChild(new Text(`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`, 0, 0));
1246
+ container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0));
1247
+ for (const item of displayItems) {
1248
+ if (item.type !== "toolCall") continue;
1249
+ container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
1250
+ }
1251
+ if (finalOutput) {
1252
+ container.addChild(new Spacer(1));
1253
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1254
+ }
1255
+ const fanInUsage = formatUsageStats(aggregator.usage, aggregator.model);
1256
+ if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
1257
+ }
1258
+
1259
+ const usageResults = aggregator ? [...details.results, aggregator] : details.results;
1260
+ const usageStr = formatUsageStats(aggregateUsage(usageResults));
1261
+ if (usageStr) {
1262
+ container.addChild(new Spacer(1));
1263
+ container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
1264
+ }
1265
+ return container;
1266
+ }
1267
+
1268
+ function renderParallelSubagentResult(details: SubagentDetails, expanded: boolean, theme: any, mdTheme: any, renderDisplayItems: (items: DisplayItem[], limit?: number) => string): any {
1269
+ const running = details.results.filter((r) => r.exitCode === -1).length;
1270
+ const successCount = details.results.filter((r) => r.exitCode === 0).length;
1271
+ const failCount = details.results.filter((r) => r.exitCode > 0).length;
1272
+ const aggregator = details.aggregator;
1273
+ const aggregatorRunning = aggregator?.exitCode === -1;
1274
+ const aggregatorFailed = aggregator ? aggregator.exitCode > 0 || aggregator.stopReason === "error" : false;
1275
+ const isRunning = running > 0 || aggregatorRunning;
1276
+ if (expanded && !isRunning) {
1277
+ return renderParallelExpandedSubagentResult(details, theme, mdTheme);
1278
+ }
1279
+ const icon = isRunning ? theme.fg("warning", "⏳") : failCount > 0 || aggregatorFailed ? theme.fg("warning", "◐") : theme.fg("success", "✓");
1280
+ const status = isRunning
1281
+ ? aggregatorRunning
1282
+ ? `${successCount + failCount}/${details.results.length} done, fan-in running`
1283
+ : `${successCount + failCount}/${details.results.length} done, ${running} running`
1284
+ : aggregator
1285
+ ? `${successCount}/${details.results.length} tasks + fan-in`
1286
+ : `${successCount}/${details.results.length} tasks`;
1287
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
1288
+ for (const r of details.results) {
1289
+ const rIcon = r.exitCode === -1 ? theme.fg("warning", "⏳") : r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1290
+ const displayItems = getDisplayItems(r.messages);
1291
+ text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
1292
+ if (displayItems.length === 0) text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
1293
+ else text += `\n${renderDisplayItems(displayItems, 5)}`;
1294
+ }
1295
+ if (aggregator) {
1296
+ const rIcon = aggregator.exitCode === -1 ? theme.fg("warning", "⏳") : aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1297
+ const displayItems = getDisplayItems(aggregator.messages);
1298
+ text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
1299
+ if (displayItems.length === 0) text += `\n${theme.fg("muted", aggregator.exitCode === -1 ? "(running...)" : "(no output)")}`;
1300
+ else text += `\n${renderDisplayItems(displayItems, 5)}`;
1301
+ }
1302
+ if (!isRunning) {
1303
+ const usageResults = aggregator ? [...details.results, aggregator] : details.results;
1304
+ const usageStr = formatUsageStats(aggregateUsage(usageResults));
1305
+ if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
1306
+ }
1307
+ if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1308
+ return new Text(text, 0, 0);
1309
+ }
1310
+
1311
+ function renderSubagentResult(
1312
+ result: AgentToolResult<SubagentDetails>,
1313
+ expanded: boolean,
1314
+ theme: any,
1315
+ ): any {
1316
+ const details = result.details as SubagentDetails | undefined;
1317
+ if (!details || details.results.length === 0) {
1318
+ const text = result.content[0];
1319
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
1320
+ }
1321
+ const mdTheme = getMarkdownTheme();
1322
+ const renderDisplayItems = createDisplayItemRenderer(theme, expanded);
1323
+ if (details.mode === "single" && details.results.length === 1) {
1324
+ return renderSingleSubagentResult(details, expanded, theme, mdTheme, renderDisplayItems);
1325
+ }
1326
+ if (details.mode === "chain") {
1327
+ return renderChainSubagentResult(details, expanded, theme, mdTheme, renderDisplayItems);
1328
+ }
1329
+ if (details.mode === "parallel") {
1330
+ return renderParallelSubagentResult(details, expanded, theme, mdTheme, renderDisplayItems);
1331
+ }
1332
+ const text = result.content[0];
1333
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
1334
+ }
1335
+
699
1336
  export function createSubagentsExtension(
700
1337
  pi: ExtensionAPI,
701
1338
  options: HarnessSubagentsOptions = {},
@@ -725,22 +1362,32 @@ export function createSubagentsExtension(
725
1362
  ],
726
1363
  parameters: SubagentParams,
727
1364
 
728
- async execute(toolCallId, params, signal, onUpdate, ctx) {
1365
+ async execute(toolCallId, rawParams, signal, onUpdate, ctx) {
729
1366
  const startedAt = Date.now();
730
- const agentScope: AgentScope =
731
- (params.agentScope as AgentScope | undefined) ?? defaultScope;
1367
+ const params = rawParams as SubagentToolParams;
1368
+ const agentScope: AgentScope = params.agentScope ?? defaultScope;
732
1369
  const discovery = discoverAgents(ctx.cwd, agentScope, packageRoot);
733
1370
  const agents = discovery.agents;
734
- const confirmProjectAgents =
735
- params.confirmProjectAgents ?? defaultConfirm;
736
1371
  const defaultTimeoutMs = params.timeoutMs ?? ENV_TIMEOUT_MS;
737
-
738
- const resolveSpawnAuth = async (agentName: string): Promise<SpawnAuthForward | undefined> => {
1372
+ const effectiveConfirmProjectAgents =
1373
+ params.confirmProjectAgents ?? defaultConfirm;
1374
+ const harnessAgents = collectHarnessAgents(params);
1375
+ const makeDetails =
1376
+ (mode: "single" | "parallel" | "chain") =>
1377
+ (results: SingleResult[], aggregator?: SingleResult): SubagentDetails => ({
1378
+ mode,
1379
+ agentScope,
1380
+ projectAgentsDir: discovery.projectAgentsDir,
1381
+ results,
1382
+ aggregator,
1383
+ });
1384
+ const resolveSpawnAuth = async (
1385
+ agentName: string,
1386
+ ): Promise<SpawnAuthForward | undefined> => {
739
1387
  if (!options.resolveSpawnAuth) return undefined;
740
1388
  const agent = agents.find((a) => a.name === agentName);
741
1389
  if (!agent) return undefined;
742
- const forward = await options.resolveSpawnAuth(ctx, agent);
743
- return forward;
1390
+ return options.resolveSpawnAuth(ctx, agent);
744
1391
  };
745
1392
 
746
1393
  if (options.beforeExecute) {
@@ -751,12 +1398,7 @@ export function createSubagentsExtension(
751
1398
  );
752
1399
  if (!gate.ok) {
753
1400
  return {
754
- content: [
755
- {
756
- type: "text",
757
- text: gate.message ?? "Subagent spawn blocked by harness policy.",
758
- },
759
- ],
1401
+ content: [{ type: "text" as const, text: gate.message ?? "Subagent spawn blocked by harness policy." }],
760
1402
  details: {
761
1403
  mode: "single",
762
1404
  agentScope,
@@ -768,326 +1410,61 @@ export function createSubagentsExtension(
768
1410
  }
769
1411
  }
770
1412
 
771
- const harnessAgents: string[] = [];
772
- if (params.agent?.startsWith("harness/")) harnessAgents.push(params.agent);
773
- if (params.tasks)
774
- for (const t of params.tasks)
775
- if (t.agent.startsWith("harness/")) harnessAgents.push(t.agent);
776
- if (params.chain)
777
- for (const c of params.chain)
778
- if (c.agent.startsWith("harness/")) harnessAgents.push(c.agent);
779
- if (params.aggregator?.agent.startsWith("harness/"))
780
- harnessAgents.push(params.aggregator.agent);
781
1413
  options.onSpawnStart?.(harnessAgents.length);
782
-
783
1414
  try {
784
- const hasChain = (params.chain?.length ?? 0) > 0;
785
- const hasTasks = (params.tasks?.length ?? 0) > 0;
786
- const hasSingle = Boolean(params.agent && params.task);
787
- const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
788
-
789
- const makeDetails =
790
- (mode: "single" | "parallel" | "chain") =>
791
- (results: SingleResult[], aggregator?: SingleResult): SubagentDetails => ({
792
- mode,
793
- agentScope,
794
- projectAgentsDir: discovery.projectAgentsDir,
795
- results,
796
- aggregator,
797
- });
798
-
799
- if (modeCount !== 1 || (params.aggregator && !hasTasks)) {
800
- const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
801
- const reason =
802
- modeCount !== 1
803
- ? "Provide exactly one mode."
804
- : "Aggregator is only valid with parallel tasks.";
805
- return {
806
- content: [
807
- {
808
- type: "text",
809
- text: `Invalid parameters. ${reason}\nAvailable agents: ${available}`,
810
- },
811
- ],
812
- details: makeDetails("single")([]),
813
- };
814
- }
815
-
816
- if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
817
- const requestedAgentNames = new Set<string>();
818
- if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
819
- if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
820
- if (params.aggregator) requestedAgentNames.add(params.aggregator.agent);
821
- if (params.agent) requestedAgentNames.add(params.agent);
822
-
823
- const projectAgentsRequested = Array.from(requestedAgentNames)
824
- .map((name) => agents.find((a) => a.name === name))
825
- .filter((a): a is AgentConfig => a?.source === "project");
826
-
827
- if (projectAgentsRequested.length > 0) {
828
- const names = projectAgentsRequested.map((a) => a.name).join(", ");
829
- const dir = discovery.projectAgentsDir ?? "(unknown)";
830
- const ok = await ctx.ui.confirm(
831
- "Run project-local agents?",
832
- `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
833
- );
834
- if (!ok)
835
- return {
836
- content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
837
- details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
838
- };
839
- }
840
- }
841
-
842
- if (params.chain && params.chain.length > 0) {
843
- const results: SingleResult[] = [];
844
- let previousOutput = "";
845
- const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, params.chain.length));
846
-
847
- try {
848
- for (let i = 0; i < params.chain.length; i++) {
849
- const step = params.chain[i];
850
- status.update(chainStatus(i + 1, params.chain.length, step.agent));
851
- const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
852
-
853
- // Create update callback that includes all previous results
854
- const chainUpdate: OnUpdateCallback | undefined = onUpdate
855
- ? (partial) => {
856
- // Combine completed results with current streaming result
857
- const currentResult = partial.details?.results[0];
858
- if (currentResult) {
859
- const allResults = [...results, currentResult];
860
- onUpdate({
861
- content: partial.content,
862
- details: makeDetails("chain")(allResults),
863
- });
864
- }
865
- }
866
- : undefined;
867
-
868
- const result = await runSingleAgent(
869
- ctx.cwd,
870
- agents,
871
- step.agent,
872
- taskWithContext,
873
- step.cwd,
874
- i + 1,
875
- signal,
876
- step.timeoutMs ?? defaultTimeoutMs,
877
- chainUpdate,
878
- makeDetails("chain"),
879
- packageRoot,
880
- await resolveSpawnAuth(step.agent),
881
- options,
882
- );
883
- results.push(result);
884
-
885
- const isError =
886
- result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
887
- if (isError) {
888
- const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
889
- return {
890
- content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
891
- details: makeDetails("chain")(results),
892
- isError: true,
893
- };
894
- }
895
- previousOutput = getResultFinalOutput(result);
896
- }
1415
+ const mode = modeInfo(params);
1416
+ if (mode.modeCount !== 1 || (params.aggregator && !mode.hasTasks)) {
1417
+ const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
1418
+ const reason =
1419
+ mode.modeCount !== 1
1420
+ ? "Provide exactly one mode."
1421
+ : "Aggregator is only valid with parallel tasks.";
897
1422
  return {
898
- content: [{ type: "text", text: getResultFinalOutput(results[results.length - 1]) || "(no output)" }],
899
- details: makeDetails("chain")(results),
1423
+ content: [{ type: "text" as const, text: `Invalid parameters. ${reason}\nAvailable agents: ${available}` }],
1424
+ details: makeDetails("single")([]),
900
1425
  };
901
- } finally {
902
- status.clear();
903
1426
  }
904
- }
905
-
906
- if (params.tasks && params.tasks.length > 0) {
907
- if (params.tasks.length > MAX_PARALLEL_TASKS)
908
- return {
909
- content: [
910
- {
911
- type: "text",
912
- text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
913
- },
914
- ],
915
- details: makeDetails("parallel")([]),
916
- };
917
1427
 
918
- const status = startSubagentStatus(ctx, toolCallId, parallelStatus(0, params.tasks.length, params.tasks.length));
919
-
920
- try {
921
- // Track all results for streaming updates
922
- const allResults: SingleResult[] = new Array(params.tasks.length);
923
-
924
- // Initialize placeholder results
925
- for (let i = 0; i < params.tasks.length; i++) {
926
- allResults[i] = {
927
- agent: params.tasks[i].agent,
928
- agentSource: "unknown",
929
- task: params.tasks[i].task,
930
- exitCode: -1, // -1 = still running
931
- messages: [],
932
- stderr: "",
933
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
934
- finalOutput: "",
935
- };
936
- }
937
-
938
- let doneCount = 0;
939
- let runningCount = params.tasks.length;
940
-
941
- const emitParallelUpdate = () => {
942
- status.update(parallelStatus(doneCount, allResults.length, runningCount));
943
- if (onUpdate) {
944
- onUpdate({
945
- content: [
946
- {
947
- type: "text",
948
- text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...`,
949
- },
950
- ],
951
- details: makeDetails("parallel")([...allResults]),
952
- });
953
- }
954
- };
955
-
956
- const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
957
- const result = await runSingleAgent(
958
- ctx.cwd,
959
- agents,
960
- t.agent,
961
- t.task,
962
- t.cwd,
963
- undefined,
964
- signal,
965
- t.timeoutMs ?? defaultTimeoutMs,
966
- // Per-task update callback
967
- (partial) => {
968
- if (partial.details?.results[0]) {
969
- allResults[index] = { ...partial.details.results[0], exitCode: -1 };
970
- emitParallelUpdate();
971
- }
972
- },
973
- makeDetails("parallel"),
974
- packageRoot,
975
- await resolveSpawnAuth(t.agent),
976
- options,
977
- );
978
- allResults[index] = result;
979
- doneCount += 1;
980
- runningCount -= 1;
981
- emitParallelUpdate();
982
- return result;
983
- });
1428
+ const execCtx: SubagentExecuteContext = {
1429
+ toolCallId,
1430
+ params: {
1431
+ ...params,
1432
+ confirmProjectAgents: effectiveConfirmProjectAgents,
1433
+ },
1434
+ signal,
1435
+ onUpdate,
1436
+ ctx,
1437
+ agents,
1438
+ discovery,
1439
+ agentScope,
1440
+ defaultTimeoutMs,
1441
+ packageRoot,
1442
+ options,
1443
+ resolveSpawnAuth,
1444
+ makeDetails,
1445
+ };
984
1446
 
985
- let aggregatorResult: SingleResult | undefined;
986
- if (params.aggregator) {
987
- const aggregator = params.aggregator;
988
- status.update(fanInStatus(aggregator.agent));
989
- const fanInContext = buildFanInContext(results);
990
- const aggregatorTask = aggregator.task.includes("{previous}")
991
- ? aggregator.task.replace(/\{previous\}/g, fanInContext)
992
- : `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`;
993
- aggregatorResult = await runSingleAgent(
994
- ctx.cwd,
995
- agents,
996
- aggregator.agent,
997
- aggregatorTask,
998
- aggregator.cwd,
999
- undefined,
1000
- signal,
1001
- aggregator.timeoutMs ?? defaultTimeoutMs,
1002
- (partial) => {
1003
- status.update(fanInStatus(aggregator.agent));
1004
- if (onUpdate && partial.details?.results[0]) {
1005
- onUpdate({
1006
- content: partial.content,
1007
- details: makeDetails("parallel")(results, partial.details.results[0]),
1008
- });
1009
- }
1010
- },
1011
- makeDetails("parallel"),
1012
- packageRoot,
1013
- await resolveSpawnAuth(aggregator.agent),
1014
- options,
1015
- );
1016
- }
1017
-
1018
- const successCount = results.filter((r) => r.exitCode === 0).length;
1019
- const summaries = results.map((r) => {
1020
- const output = getResultFinalOutput(r);
1021
- const error = r.errorMessage || r.stderr.trim();
1022
- const summaryText = output || error;
1023
- const preview = summaryText.slice(0, 160) + (summaryText.length > 160 ? "..." : "");
1024
- return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
1025
- });
1026
- const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
1027
- const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
1447
+ const uiMode: "single" | "parallel" | "chain" = mode.hasChain
1448
+ ? "chain"
1449
+ : mode.hasTasks
1450
+ ? "parallel"
1451
+ : "single";
1452
+ if (!(await maybeConfirmProjectAgents(execCtx, uiMode))) {
1028
1453
  return {
1029
- content: [
1030
- {
1031
- type: "text",
1032
- text: aggregatorResult
1033
- ? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
1034
- : `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
1035
- },
1036
- ],
1037
- details: makeDetails("parallel")(results, aggregatorResult),
1038
- isError: aggregatorResult
1039
- ? aggregatorResult.exitCode !== 0 ||
1040
- aggregatorResult.stopReason === "error" ||
1041
- aggregatorResult.stopReason === "aborted"
1042
- : undefined,
1454
+ content: [{ type: "text" as const, text: "Canceled: project-local agents not approved." }],
1455
+ details: makeDetails(uiMode)([]),
1043
1456
  };
1044
- } finally {
1045
- status.clear();
1046
1457
  }
1047
- }
1048
1458
 
1049
- if (params.agent && params.task) {
1050
- const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent));
1051
-
1052
- try {
1053
- const result = await runSingleAgent(
1054
- ctx.cwd,
1055
- agents,
1056
- params.agent,
1057
- params.task,
1058
- params.cwd,
1059
- undefined,
1060
- signal,
1061
- params.timeoutMs ?? defaultTimeoutMs,
1062
- onUpdate,
1063
- makeDetails("single"),
1064
- packageRoot,
1065
- await resolveSpawnAuth(params.agent),
1066
- options,
1067
- );
1068
- const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
1069
- if (isError) {
1070
- const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
1071
- return {
1072
- content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
1073
- details: makeDetails("single")([result]),
1074
- isError: true,
1075
- };
1076
- }
1077
- return {
1078
- content: [{ type: "text", text: getResultFinalOutput(result) || "(no output)" }],
1079
- details: makeDetails("single")([result]),
1080
- };
1081
- } finally {
1082
- status.clear();
1083
- }
1084
- }
1459
+ if (mode.hasChain) return executeChainMode(execCtx);
1460
+ if (mode.hasTasks) return executeParallelMode(execCtx);
1461
+ if (mode.hasSingle) return executeSingleMode(execCtx);
1085
1462
 
1086
- const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
1087
- return {
1088
- content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
1089
- details: makeDetails("single")([]),
1090
- };
1463
+ const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
1464
+ return {
1465
+ content: [{ type: "text" as const, text: `Invalid parameters. Available agents: ${available}` }],
1466
+ details: makeDetails("single")([]),
1467
+ };
1091
1468
  } finally {
1092
1469
  options.onSpawnEnd?.(harnessAgents.length);
1093
1470
  const mode = params.chain?.length
@@ -1157,329 +1534,7 @@ export function createSubagentsExtension(
1157
1534
  },
1158
1535
 
1159
1536
  renderResult(result, { expanded }, theme, _context) {
1160
- const details = result.details as SubagentDetails | undefined;
1161
- if (!details || details.results.length === 0) {
1162
- const text = result.content[0];
1163
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
1164
- }
1165
-
1166
- const mdTheme = getMarkdownTheme();
1167
-
1168
- const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
1169
- const toShow = limit ? items.slice(-limit) : items;
1170
- const skipped = limit && items.length > limit ? items.length - limit : 0;
1171
- let text = "";
1172
- if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
1173
- for (const item of toShow) {
1174
- if (item.type === "text") {
1175
- const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
1176
- text += `${theme.fg("toolOutput", preview)}\n`;
1177
- } else {
1178
- text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
1179
- }
1180
- }
1181
- return text.trimEnd();
1182
- };
1183
-
1184
- if (details.mode === "single" && details.results.length === 1) {
1185
- const r = details.results[0];
1186
- const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
1187
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
1188
- const displayItems = getDisplayItems(r.messages);
1189
- const finalOutput = getResultFinalOutput(r);
1190
-
1191
- if (expanded) {
1192
- const container = new Container();
1193
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
1194
- if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
1195
- container.addChild(new Text(header, 0, 0));
1196
- if (isError && r.errorMessage)
1197
- container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
1198
- container.addChild(new Spacer(1));
1199
- container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
1200
- container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
1201
- container.addChild(new Spacer(1));
1202
- container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
1203
- if (displayItems.length === 0 && !finalOutput) {
1204
- container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
1205
- } else {
1206
- for (const item of displayItems) {
1207
- if (item.type === "toolCall")
1208
- container.addChild(
1209
- new Text(
1210
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
1211
- 0,
1212
- 0,
1213
- ),
1214
- );
1215
- }
1216
- if (finalOutput) {
1217
- container.addChild(new Spacer(1));
1218
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1219
- }
1220
- }
1221
- const usageStr = formatUsageStats(r.usage, r.model);
1222
- if (usageStr) {
1223
- container.addChild(new Spacer(1));
1224
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
1225
- }
1226
- return container;
1227
- }
1228
-
1229
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
1230
- if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
1231
- if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
1232
- else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
1233
- else {
1234
- text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
1235
- if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1236
- }
1237
- const usageStr = formatUsageStats(r.usage, r.model);
1238
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
1239
- return new Text(text, 0, 0);
1240
- }
1241
-
1242
- const aggregateUsage = (results: SingleResult[]) => {
1243
- const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
1244
- for (const r of results) {
1245
- total.input += r.usage.input;
1246
- total.output += r.usage.output;
1247
- total.cacheRead += r.usage.cacheRead;
1248
- total.cacheWrite += r.usage.cacheWrite;
1249
- total.cost += r.usage.cost;
1250
- total.turns += r.usage.turns;
1251
- }
1252
- return total;
1253
- };
1254
-
1255
- if (details.mode === "chain") {
1256
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
1257
- const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
1258
-
1259
- if (expanded) {
1260
- const container = new Container();
1261
- container.addChild(
1262
- new Text(
1263
- icon +
1264
- " " +
1265
- theme.fg("toolTitle", theme.bold("chain ")) +
1266
- theme.fg("accent", `${successCount}/${details.results.length} steps`),
1267
- 0,
1268
- 0,
1269
- ),
1270
- );
1271
-
1272
- for (const r of details.results) {
1273
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1274
- const displayItems = getDisplayItems(r.messages);
1275
- const finalOutput = getResultFinalOutput(r);
1276
-
1277
- container.addChild(new Spacer(1));
1278
- container.addChild(
1279
- new Text(
1280
- `${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
1281
- 0,
1282
- 0,
1283
- ),
1284
- );
1285
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
1286
-
1287
- // Show tool calls
1288
- for (const item of displayItems) {
1289
- if (item.type === "toolCall") {
1290
- container.addChild(
1291
- new Text(
1292
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
1293
- 0,
1294
- 0,
1295
- ),
1296
- );
1297
- }
1298
- }
1299
-
1300
- // Show final output as markdown
1301
- if (finalOutput) {
1302
- container.addChild(new Spacer(1));
1303
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1304
- }
1305
-
1306
- const stepUsage = formatUsageStats(r.usage, r.model);
1307
- if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
1308
- }
1309
-
1310
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1311
- if (usageStr) {
1312
- container.addChild(new Spacer(1));
1313
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
1314
- }
1315
- return container;
1316
- }
1317
-
1318
- // Collapsed view
1319
- let text =
1320
- icon +
1321
- " " +
1322
- theme.fg("toolTitle", theme.bold("chain ")) +
1323
- theme.fg("accent", `${successCount}/${details.results.length} steps`);
1324
- for (const r of details.results) {
1325
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1326
- const displayItems = getDisplayItems(r.messages);
1327
- text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
1328
- if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
1329
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
1330
- }
1331
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1332
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
1333
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1334
- return new Text(text, 0, 0);
1335
- }
1336
-
1337
- if (details.mode === "parallel") {
1338
- const running = details.results.filter((r) => r.exitCode === -1).length;
1339
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
1340
- const failCount = details.results.filter((r) => r.exitCode > 0).length;
1341
- const aggregator = details.aggregator;
1342
- const aggregatorRunning = aggregator?.exitCode === -1;
1343
- const aggregatorFailed = aggregator ? aggregator.exitCode > 0 || aggregator.stopReason === "error" : false;
1344
- const isRunning = running > 0 || aggregatorRunning;
1345
- const icon = isRunning
1346
- ? theme.fg("warning", "⏳")
1347
- : failCount > 0 || aggregatorFailed
1348
- ? theme.fg("warning", "◐")
1349
- : theme.fg("success", "✓");
1350
- const status = isRunning
1351
- ? aggregatorRunning
1352
- ? `${successCount + failCount}/${details.results.length} done, fan-in running`
1353
- : `${successCount + failCount}/${details.results.length} done, ${running} running`
1354
- : aggregator
1355
- ? `${successCount}/${details.results.length} tasks + fan-in`
1356
- : `${successCount}/${details.results.length} tasks`;
1357
-
1358
- if (expanded && !isRunning) {
1359
- const container = new Container();
1360
- container.addChild(
1361
- new Text(
1362
- `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
1363
- 0,
1364
- 0,
1365
- ),
1366
- );
1367
-
1368
- for (const r of details.results) {
1369
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1370
- const displayItems = getDisplayItems(r.messages);
1371
- const finalOutput = getResultFinalOutput(r);
1372
-
1373
- container.addChild(new Spacer(1));
1374
- container.addChild(
1375
- new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
1376
- );
1377
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
1378
-
1379
- // Show tool calls
1380
- for (const item of displayItems) {
1381
- if (item.type === "toolCall") {
1382
- container.addChild(
1383
- new Text(
1384
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
1385
- 0,
1386
- 0,
1387
- ),
1388
- );
1389
- }
1390
- }
1391
-
1392
- // Show final output as markdown
1393
- if (finalOutput) {
1394
- container.addChild(new Spacer(1));
1395
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1396
- }
1397
-
1398
- const taskUsage = formatUsageStats(r.usage, r.model);
1399
- if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
1400
- }
1401
-
1402
- if (aggregator) {
1403
- const rIcon = aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1404
- const displayItems = getDisplayItems(aggregator.messages);
1405
- const finalOutput = getResultFinalOutput(aggregator);
1406
-
1407
- container.addChild(new Spacer(1));
1408
- container.addChild(
1409
- new Text(
1410
- `${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
1411
- 0,
1412
- 0,
1413
- ),
1414
- );
1415
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0));
1416
- for (const item of displayItems) {
1417
- if (item.type === "toolCall") {
1418
- container.addChild(
1419
- new Text(
1420
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
1421
- 0,
1422
- 0,
1423
- ),
1424
- );
1425
- }
1426
- }
1427
- if (finalOutput) {
1428
- container.addChild(new Spacer(1));
1429
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1430
- }
1431
- const fanInUsage = formatUsageStats(aggregator.usage, aggregator.model);
1432
- if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
1433
- }
1434
-
1435
- const usageResults = aggregator ? [...details.results, aggregator] : details.results;
1436
- const usageStr = formatUsageStats(aggregateUsage(usageResults));
1437
- if (usageStr) {
1438
- container.addChild(new Spacer(1));
1439
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
1440
- }
1441
- return container;
1442
- }
1443
-
1444
- // Collapsed view (or still running)
1445
- let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
1446
- for (const r of details.results) {
1447
- const rIcon =
1448
- r.exitCode === -1
1449
- ? theme.fg("warning", "⏳")
1450
- : r.exitCode === 0
1451
- ? theme.fg("success", "✓")
1452
- : theme.fg("error", "✗");
1453
- const displayItems = getDisplayItems(r.messages);
1454
- text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
1455
- if (displayItems.length === 0)
1456
- text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
1457
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
1458
- }
1459
- if (aggregator) {
1460
- const rIcon =
1461
- aggregator.exitCode === -1
1462
- ? theme.fg("warning", "⏳")
1463
- : aggregator.exitCode === 0
1464
- ? theme.fg("success", "✓")
1465
- : theme.fg("error", "✗");
1466
- const displayItems = getDisplayItems(aggregator.messages);
1467
- text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
1468
- if (displayItems.length === 0)
1469
- text += `\n${theme.fg("muted", aggregator.exitCode === -1 ? "(running...)" : "(no output)")}`;
1470
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
1471
- }
1472
- if (!isRunning) {
1473
- const usageResults = aggregator ? [...details.results, aggregator] : details.results;
1474
- const usageStr = formatUsageStats(aggregateUsage(usageResults));
1475
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
1476
- }
1477
- if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1478
- return new Text(text, 0, 0);
1479
- }
1480
-
1481
- const text = result.content[0];
1482
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
1537
+ return renderSubagentResult(result as AgentToolResult<SubagentDetails>, expanded, theme);
1483
1538
  },
1484
1539
  });
1485
1540
  }