iosm-cli 0.2.0 → 0.2.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 (90) hide show
  1. package/README.md +58 -47
  2. package/dist/core/agent-teams.d.ts.map +1 -1
  3. package/dist/core/agent-teams.js +38 -11
  4. package/dist/core/agent-teams.js.map +1 -1
  5. package/dist/core/failure-retrospective.d.ts +12 -0
  6. package/dist/core/failure-retrospective.d.ts.map +1 -0
  7. package/dist/core/failure-retrospective.js +115 -0
  8. package/dist/core/failure-retrospective.js.map +1 -0
  9. package/dist/core/project-index/index.d.ts +17 -0
  10. package/dist/core/project-index/index.d.ts.map +1 -0
  11. package/dist/core/project-index/index.js +323 -0
  12. package/dist/core/project-index/index.js.map +1 -0
  13. package/dist/core/project-index/types.d.ts +34 -0
  14. package/dist/core/project-index/types.d.ts.map +1 -0
  15. package/dist/core/project-index/types.js +2 -0
  16. package/dist/core/project-index/types.js.map +1 -0
  17. package/dist/core/sdk.d.ts.map +1 -1
  18. package/dist/core/sdk.js +8 -0
  19. package/dist/core/sdk.js.map +1 -1
  20. package/dist/core/shared-memory.d.ts +46 -0
  21. package/dist/core/shared-memory.d.ts.map +1 -0
  22. package/dist/core/shared-memory.js +253 -0
  23. package/dist/core/shared-memory.js.map +1 -0
  24. package/dist/core/slash-commands.d.ts.map +1 -1
  25. package/dist/core/slash-commands.js +5 -1
  26. package/dist/core/slash-commands.js.map +1 -1
  27. package/dist/core/subagents.js +1 -1
  28. package/dist/core/subagents.js.map +1 -1
  29. package/dist/core/swarm/gates.d.ts +9 -0
  30. package/dist/core/swarm/gates.d.ts.map +1 -0
  31. package/dist/core/swarm/gates.js +65 -0
  32. package/dist/core/swarm/gates.js.map +1 -0
  33. package/dist/core/swarm/index.d.ts +9 -0
  34. package/dist/core/swarm/index.d.ts.map +1 -0
  35. package/dist/core/swarm/index.js +9 -0
  36. package/dist/core/swarm/index.js.map +1 -0
  37. package/dist/core/swarm/locks.d.ts +21 -0
  38. package/dist/core/swarm/locks.d.ts.map +1 -0
  39. package/dist/core/swarm/locks.js +93 -0
  40. package/dist/core/swarm/locks.js.map +1 -0
  41. package/dist/core/swarm/planner.d.ts +16 -0
  42. package/dist/core/swarm/planner.d.ts.map +1 -0
  43. package/dist/core/swarm/planner.js +137 -0
  44. package/dist/core/swarm/planner.js.map +1 -0
  45. package/dist/core/swarm/retry.d.ts +16 -0
  46. package/dist/core/swarm/retry.d.ts.map +1 -0
  47. package/dist/core/swarm/retry.js +32 -0
  48. package/dist/core/swarm/retry.js.map +1 -0
  49. package/dist/core/swarm/scheduler.d.ts +48 -0
  50. package/dist/core/swarm/scheduler.d.ts.map +1 -0
  51. package/dist/core/swarm/scheduler.js +554 -0
  52. package/dist/core/swarm/scheduler.js.map +1 -0
  53. package/dist/core/swarm/spawn.d.ts +16 -0
  54. package/dist/core/swarm/spawn.d.ts.map +1 -0
  55. package/dist/core/swarm/spawn.js +42 -0
  56. package/dist/core/swarm/spawn.js.map +1 -0
  57. package/dist/core/swarm/state-store.d.ts +35 -0
  58. package/dist/core/swarm/state-store.d.ts.map +1 -0
  59. package/dist/core/swarm/state-store.js +106 -0
  60. package/dist/core/swarm/state-store.js.map +1 -0
  61. package/dist/core/swarm/types.d.ts +116 -0
  62. package/dist/core/swarm/types.d.ts.map +1 -0
  63. package/dist/core/swarm/types.js +2 -0
  64. package/dist/core/swarm/types.js.map +1 -0
  65. package/dist/core/system-prompt.d.ts.map +1 -1
  66. package/dist/core/system-prompt.js +3 -2
  67. package/dist/core/system-prompt.js.map +1 -1
  68. package/dist/core/tools/shared-memory.d.ts +23 -0
  69. package/dist/core/tools/shared-memory.d.ts.map +1 -0
  70. package/dist/core/tools/shared-memory.js +134 -0
  71. package/dist/core/tools/shared-memory.js.map +1 -0
  72. package/dist/core/tools/task.d.ts +8 -1
  73. package/dist/core/tools/task.d.ts.map +1 -1
  74. package/dist/core/tools/task.js +664 -123
  75. package/dist/core/tools/task.js.map +1 -1
  76. package/dist/modes/interactive/components/login-dialog.d.ts +1 -0
  77. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/login-dialog.js +27 -4
  79. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  80. package/dist/modes/interactive/components/subagent-message.d.ts.map +1 -1
  81. package/dist/modes/interactive/components/subagent-message.js +14 -0
  82. package/dist/modes/interactive/components/subagent-message.js.map +1 -1
  83. package/dist/modes/interactive/interactive-mode.d.ts +38 -0
  84. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  85. package/dist/modes/interactive/interactive-mode.js +1362 -31
  86. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  87. package/docs/cli-reference.md +11 -1
  88. package/docs/interactive-mode.md +42 -3
  89. package/docs/orchestration-and-subagents.md +96 -169
  90. package/package.json +4 -3
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import { getTeamRun, updateTeamTaskStatus } from "../agent-teams.js";
6
+ import { buildRetrospectiveDirective, classifyFailureCause, formatFailureCauseCounts, isRetrospectiveRetryable, } from "../failure-retrospective.js";
6
7
  const taskSchema = Type.Object({
7
8
  description: Type.String({
8
9
  description: "Short 3-5 word description of what the subagent will do",
@@ -13,16 +14,8 @@ const taskSchema = Type.Object({
13
14
  agent: Type.Optional(Type.String({
14
15
  description: "Optional custom subagent name loaded from .iosm/agents or global agents directory.",
15
16
  })),
16
- profile: Type.Union([
17
- Type.Literal("explore"),
18
- Type.Literal("plan"),
19
- Type.Literal("iosm"),
20
- Type.Literal("iosm_analyst"),
21
- Type.Literal("iosm_verifier"),
22
- Type.Literal("cycle_planner"),
23
- Type.Literal("full"),
24
- ], {
25
- description: "Subagent capability profile: explore (read-only), plan (read + bash, no edits), iosm (full tools + IOSM methodology), iosm_analyst (read + bash for IOSM artifacts), iosm_verifier (artifact-focused checks), cycle_planner (cycle planning), full (all tools)",
17
+ profile: Type.String({
18
+ description: "Subagent capability profile. Recommended values: explore, plan, iosm, iosm_analyst, iosm_verifier, cycle_planner, full. For custom agents, pass the agent name via `agent`, not `profile`.",
26
19
  }),
27
20
  cwd: Type.Optional(Type.String({
28
21
  description: "Optional working directory for this subagent. Relative paths are resolved from the current workspace.",
@@ -31,10 +24,10 @@ const taskSchema = Type.Object({
31
24
  description: "Optional logical lock key for write serialization (e.g. src/api/**). Agents with the same lock key run write phases sequentially.",
32
25
  })),
33
26
  run_id: Type.Optional(Type.String({
34
- description: "Optional orchestration run id (from /orchestrate). Use with task_id so the team board can track status.",
27
+ description: "Optional orchestration run id (from /orchestrate or /swarm). Use with task_id so the team board can track status. When omitted, task mode uses an internal run id for shared-memory collaboration within this task execution.",
35
28
  })),
36
29
  task_id: Type.Optional(Type.String({
37
- description: "Optional orchestration task id (for example task_1). Use with run_id to update the team board.",
30
+ description: "Optional orchestration task id (for example task_1). Use with run_id to update the team board. When omitted, task mode uses an internal task id so task-scoped shared memory still works.",
38
31
  })),
39
32
  model: Type.Optional(Type.String({
40
33
  description: "Optional model override for this subagent (for example anthropic/claude-sonnet-4 or model id).",
@@ -45,6 +38,11 @@ const taskSchema = Type.Object({
45
38
  isolation: Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("worktree")], {
46
39
  description: "Optional isolation mode. Set to worktree to run this subagent in a temporary git worktree.",
47
40
  })),
41
+ delegate_parallel_hint: Type.Optional(Type.Integer({
42
+ minimum: 1,
43
+ maximum: 10,
44
+ description: "Optional hint for intra-task delegation fan-out. Higher value allows more delegated subtasks to run in parallel inside a single task execution.",
45
+ })),
48
46
  });
49
47
  /** Tool names available per profile */
50
48
  const toolsByProfile = {
@@ -94,6 +92,9 @@ class Semaphore {
94
92
  next();
95
93
  }
96
94
  }
95
+ isIdle() {
96
+ return this.active === 0 && this.queue.length === 0;
97
+ }
97
98
  }
98
99
  class Mutex {
99
100
  constructor() {
@@ -116,12 +117,19 @@ class Mutex {
116
117
  next();
117
118
  }
118
119
  }
120
+ isIdle() {
121
+ return !this.locked && this.waiters.length === 0;
122
+ }
119
123
  }
120
- const maxParallelFromEnv = Number.parseInt(process.env.IOSM_SUBAGENT_MAX_PARALLEL ?? "4", 10);
121
- const subagentSemaphore = new Semaphore(Number.isInteger(maxParallelFromEnv) && maxParallelFromEnv > 0 ? Math.min(maxParallelFromEnv, 20) : 4);
124
+ const maxParallelFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_PARALLEL, 20, 1, 20);
125
+ const subagentSemaphore = new Semaphore(maxParallelFromEnv);
122
126
  const maxDelegationDepthFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATION_DEPTH, 1, 0, 3);
123
- const maxDelegationsPerTaskFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATIONS_PER_TASK, 2, 0, 10);
124
- const maxDelegatedParallelFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATE_PARALLEL, 4, 1, 10);
127
+ const maxDelegationsPerTaskFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATIONS_PER_TASK, 10, 0, 10);
128
+ const maxDelegatedParallelFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATE_PARALLEL, 10, 1, 10);
129
+ const emptyOutputRetriesFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_EMPTY_OUTPUT_RETRIES, 1, 0, 2);
130
+ const retrospectiveRetriesFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_RETRO_RETRIES, 1, 0, 1);
131
+ const orchestrationDependencyWaitTimeoutMsFromEnv = parseBoundedInt(process.env.IOSM_ORCHESTRATION_DEPENDENCY_WAIT_TIMEOUT_MS, 120_000, 5_000, 900_000);
132
+ const orchestrationDependencyPollMsFromEnv = parseBoundedInt(process.env.IOSM_ORCHESTRATION_DEPENDENCY_POLL_MS, 150, 50, 2_000);
125
133
  const maxDelegatedOutputCharsFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_DELEGATED_OUTPUT_MAX_CHARS, 6000, 500, 20_000);
126
134
  const maxMetaUpdatesPerCheckpoint = parseBoundedInt(process.env.IOSM_SUBAGENT_META_MAX_ITEMS, 5, 1, 20);
127
135
  const maxMetaUpdateChars = parseBoundedInt(process.env.IOSM_SUBAGENT_META_MAX_CHARS, 600, 100, 4000);
@@ -133,6 +141,65 @@ function parseBoundedInt(raw, fallback, min, max) {
133
141
  return fallback;
134
142
  return Math.max(min, Math.min(max, parsed));
135
143
  }
144
+ function shouldAutoDelegateByAgent(agentName) {
145
+ if (!agentName)
146
+ return false;
147
+ const normalized = agentName.trim().toLowerCase();
148
+ return normalized.includes("orchestrator");
149
+ }
150
+ function deriveAutoDelegateParallelHint(agentName, description, prompt) {
151
+ if (!shouldAutoDelegateByAgent(agentName))
152
+ return undefined;
153
+ const text = `${description}\n${prompt}`.trim();
154
+ if (!text)
155
+ return 1;
156
+ const normalized = text.replace(/\s+/g, " ").trim();
157
+ const words = normalized.length > 0 ? normalized.split(/\s+/).length : 0;
158
+ const clauses = normalized
159
+ .split(/[.;:,\n]+/g)
160
+ .map((item) => item.trim())
161
+ .filter((item) => item.length > 0).length;
162
+ const pathLikeMatches = normalized.match(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g) ?? [];
163
+ const fileLikeMatches = normalized.match(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,8}\b/g) ?? [];
164
+ const listMarkers = text.match(/(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/g)?.length ?? 0;
165
+ const hasCodeBlock = text.includes("```");
166
+ let score = 0;
167
+ if (words >= 40) {
168
+ score += 2;
169
+ }
170
+ else if (words >= 20) {
171
+ score += 1;
172
+ }
173
+ if (clauses >= 5) {
174
+ score += 2;
175
+ }
176
+ else if (clauses >= 3) {
177
+ score += 1;
178
+ }
179
+ if (listMarkers >= 2) {
180
+ score += 1;
181
+ }
182
+ const referenceCount = pathLikeMatches.length + fileLikeMatches.length;
183
+ if (referenceCount >= 3 || (referenceCount >= 1 && words >= 20)) {
184
+ score += 1;
185
+ }
186
+ if (hasCodeBlock) {
187
+ score += 1;
188
+ }
189
+ if (score >= 6)
190
+ return 10;
191
+ if (score >= 5)
192
+ return 8;
193
+ if (score >= 4)
194
+ return 6;
195
+ if (score >= 3)
196
+ return 4;
197
+ if (score >= 2)
198
+ return 3;
199
+ if (score >= 1)
200
+ return 2;
201
+ return 1;
202
+ }
136
203
  function normalizeSpacing(text) {
137
204
  return text.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
138
205
  }
@@ -172,7 +239,16 @@ function isAbortError(error) {
172
239
  }
173
240
  return false;
174
241
  }
175
- function buildDelegationProtocolPrompt(depthRemaining) {
242
+ function mergeRunStats(base, next) {
243
+ if (!base && !next)
244
+ return undefined;
245
+ return {
246
+ toolCallsStarted: (base?.toolCallsStarted ?? 0) + (next?.toolCallsStarted ?? 0),
247
+ toolCallsCompleted: (base?.toolCallsCompleted ?? 0) + (next?.toolCallsCompleted ?? 0),
248
+ assistantMessages: (base?.assistantMessages ?? 0) + (next?.assistantMessages ?? 0),
249
+ };
250
+ }
251
+ function buildDelegationProtocolPrompt(depthRemaining, maxDelegations) {
176
252
  if (depthRemaining <= 0) {
177
253
  return [
178
254
  `Delegation protocol: depth limit reached.`,
@@ -180,17 +256,31 @@ function buildDelegationProtocolPrompt(depthRemaining) {
180
256
  ].join("\n");
181
257
  }
182
258
  return [
183
- `Delegation protocol (optional): if you discover a concrete follow-up that is better done by a separate specialist, emit up to ${maxDelegationsPerTaskFromEnv} XML block(s):`,
184
- `<${delegationTagName} profile="explore|plan|iosm|iosm_analyst|iosm_verifier|cycle_planner|full" description="short title" cwd="optional relative path" lock_key="optional lock key" model="optional model override" isolation="none|worktree" depends_on="optional indices like 1|3">`,
259
+ `Delegation protocol (optional): if you discover concrete independent follow-ups, emit up to ${maxDelegations} XML block(s):`,
260
+ `<${delegationTagName} profile="explore|plan|iosm|iosm_analyst|iosm_verifier|cycle_planner|full" agent="optional custom subagent name" description="short title" cwd="optional relative path" lock_key="optional lock key" model="optional model override" isolation="none|worktree" depends_on="optional indices like 1|3">`,
185
261
  "Detailed delegated task prompt",
186
262
  `</${delegationTagName}>`,
187
263
  `Only emit blocks when necessary. Keep normal analysis/answer text outside those blocks.`,
264
+ `When shared_memory tools are available, exchange intermediate state through shared_memory_write/shared_memory_read instead of repeating large context.`,
188
265
  ].join("\n");
189
266
  }
190
- function withDelegationPrompt(basePrompt, depthRemaining) {
191
- const protocol = buildDelegationProtocolPrompt(depthRemaining);
267
+ function withDelegationPrompt(basePrompt, depthRemaining, maxDelegations) {
268
+ const protocol = buildDelegationProtocolPrompt(depthRemaining, maxDelegations);
192
269
  return `${basePrompt}\n\n${protocol}`;
193
270
  }
271
+ function buildSharedMemoryGuidance(runId, taskId) {
272
+ return [
273
+ "[SHARED_MEMORY]",
274
+ `run_id: ${runId}`,
275
+ `task_id: ${taskId ?? "(none)"}`,
276
+ "Use shared_memory_write/shared_memory_read to exchange intermediate state across parallel agents and delegates.",
277
+ "Guidelines:",
278
+ "- Use scope=run for cross-agent data and scope=task for task-local notes.",
279
+ "- Keep entries compact and key-based (for example: findings/auth, plan/step-1, risks/session).",
280
+ "- Read before overwrite when collaborating on the same key.",
281
+ "[/SHARED_MEMORY]",
282
+ ].join("\n");
283
+ }
194
284
  function parseDelegationRequests(output, maxRequests) {
195
285
  const requests = [];
196
286
  const warnings = [];
@@ -227,6 +317,7 @@ function parseDelegationRequests(output, maxRequests) {
227
317
  requests.push({
228
318
  description: (attrs.description ?? `delegated task ${requests.length + 1}`).trim(),
229
319
  profile,
320
+ agent: attrs.agent?.trim() || undefined,
230
321
  prompt,
231
322
  cwd: attrs.cwd?.trim() || undefined,
232
323
  lockKey: attrs.lock_key?.trim() || undefined,
@@ -260,6 +351,15 @@ function getOrCreateWriteLock(cwd) {
260
351
  cwdWriteLocks.set(key, created);
261
352
  return created;
262
353
  }
354
+ function cleanupWriteLock(lockKey) {
355
+ if (!lockKey)
356
+ return;
357
+ const key = getCwdLockKey(lockKey);
358
+ const existing = cwdWriteLocks.get(key);
359
+ if (!existing || !existing.isIdle())
360
+ return;
361
+ cwdWriteLocks.delete(key);
362
+ }
263
363
  function getRunParallelLimit(cwd, runId) {
264
364
  const teamRun = getTeamRun(cwd, runId);
265
365
  if (!teamRun)
@@ -284,6 +384,91 @@ function getOrCreateOrchestrationSemaphore(cwd, runId) {
284
384
  orchestrationSemaphores.set(key, created);
285
385
  return created;
286
386
  }
387
+ function isTeamTaskTerminal(status) {
388
+ return status === "done" || status === "error" || status === "cancelled";
389
+ }
390
+ function cleanupOrchestrationSemaphore(cwd, runId) {
391
+ const prefix = `${path.resolve(cwd).toLowerCase()}::${runId}::`;
392
+ const run = getTeamRun(cwd, runId);
393
+ const canDeleteForRun = !run || run.tasks.every((task) => isTeamTaskTerminal(task.status));
394
+ if (!canDeleteForRun)
395
+ return;
396
+ for (const [key, semaphore] of orchestrationSemaphores.entries()) {
397
+ if (!key.startsWith(prefix))
398
+ continue;
399
+ if (!semaphore.isIdle())
400
+ continue;
401
+ orchestrationSemaphores.delete(key);
402
+ }
403
+ }
404
+ function waitForWithAbort(ms, signal) {
405
+ if (!signal) {
406
+ return new Promise((resolve) => setTimeout(resolve, ms));
407
+ }
408
+ if (signal.aborted) {
409
+ return Promise.reject(new Error("Operation aborted"));
410
+ }
411
+ return new Promise((resolve, reject) => {
412
+ const timer = setTimeout(() => {
413
+ signal.removeEventListener("abort", onAbort);
414
+ resolve();
415
+ }, ms);
416
+ const onAbort = () => {
417
+ clearTimeout(timer);
418
+ signal.removeEventListener("abort", onAbort);
419
+ reject(new Error("Operation aborted"));
420
+ };
421
+ signal.addEventListener("abort", onAbort, { once: true });
422
+ });
423
+ }
424
+ async function waitForOrchestrationDependencies(input) {
425
+ const started = Date.now();
426
+ let lastWaiting = "";
427
+ while (true) {
428
+ if (input.signal?.aborted) {
429
+ throw new Error("Operation aborted");
430
+ }
431
+ const run = getTeamRun(input.cwd, input.runId);
432
+ if (!run) {
433
+ return;
434
+ }
435
+ const current = run.tasks.find((task) => task.id === input.taskId);
436
+ if (!current) {
437
+ throw new Error(`Orchestration metadata missing task ${input.taskId} in run ${input.runId}.`);
438
+ }
439
+ const dependencies = current.dependsOn ?? [];
440
+ if (dependencies.length === 0) {
441
+ return;
442
+ }
443
+ const dependencyTasks = dependencies.map((id) => run.tasks.find((task) => task.id === id));
444
+ const missing = dependencyTasks
445
+ .map((task, index) => (task ? undefined : dependencies[index]))
446
+ .filter((value) => typeof value === "string");
447
+ if (missing.length > 0) {
448
+ throw new Error(`Orchestration metadata invalid for ${input.taskId}: missing dependency task(s) ${missing.join(", ")}.`);
449
+ }
450
+ const failed = dependencyTasks.filter((task) => !!task && (task.status === "error" || task.status === "cancelled"));
451
+ if (failed.length > 0) {
452
+ throw new Error(`Blocked by failed dependency: ${failed.map((task) => `${task.id}=${task.status}`).join(", ")}.`);
453
+ }
454
+ const pending = dependencyTasks.filter((task) => !!task && task.status !== "done");
455
+ if (pending.length === 0) {
456
+ return;
457
+ }
458
+ const waitedMs = Date.now() - started;
459
+ if (waitedMs >= orchestrationDependencyWaitTimeoutMsFromEnv) {
460
+ throw new Error(`Timed out waiting for dependencies of ${input.taskId}: ${pending
461
+ .map((task) => `${task.id}=${task.status}`)
462
+ .join(", ")}.`);
463
+ }
464
+ const waiting = pending.map((task) => `${task.id}=${task.status}`).join(", ");
465
+ if (waiting !== lastWaiting) {
466
+ lastWaiting = waiting;
467
+ input.onWaiting?.(waiting);
468
+ }
469
+ await waitForWithAbort(orchestrationDependencyPollMsFromEnv, input.signal);
470
+ }
471
+ }
287
472
  function persistSubagentTranscript(input) {
288
473
  try {
289
474
  const dir = path.join(input.rootCwd, ".iosm", "subagents", "runs");
@@ -408,21 +593,54 @@ export function createTaskTool(cwd, runner, options) {
408
593
  "It may request bounded follow-up delegation via <delegate_task> blocks that are executed by the parent task tool." +
409
594
  customAgentsSnippet,
410
595
  parameters: taskSchema,
411
- execute: async (_toolCallId, { description, prompt, agent: agentName, profile, cwd: targetCwd, lock_key: lockKey, run_id: orchestrationRunId, task_id: orchestrationTaskId, model: requestedModel, background, isolation, }, _signal, onUpdate) => {
596
+ execute: async (_toolCallId, { description, prompt, agent: agentName, profile, cwd: targetCwd, lock_key: lockKey, run_id: orchestrationRunId, task_id: orchestrationTaskId, model: requestedModel, background, isolation, delegate_parallel_hint: delegateParallelHint, }, _signal, onUpdate) => {
597
+ const updateTrackedTaskStatus = (status) => {
598
+ if (!orchestrationRunId || !orchestrationTaskId)
599
+ return;
600
+ updateTeamTaskStatus({
601
+ cwd,
602
+ runId: orchestrationRunId,
603
+ taskId: orchestrationTaskId,
604
+ status,
605
+ });
606
+ };
412
607
  const throwIfAborted = () => {
413
608
  if (_signal?.aborted) {
609
+ updateTrackedTaskStatus("cancelled");
414
610
  throw new Error("Operation aborted");
415
611
  }
416
612
  };
417
613
  const runId = `subagent_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
418
- const customSubagent = agentName && options?.resolveCustomSubagent ? options.resolveCustomSubagent(agentName) : undefined;
419
- if (agentName && !customSubagent) {
420
- const available = options?.availableCustomSubagents && options.availableCustomSubagents.length > 0
421
- ? ` Available custom agents: ${options.availableCustomSubagents.join(", ")}.`
422
- : "";
423
- throw new Error(`Unknown subagent: ${agentName}.${available}`);
614
+ const sharedMemoryRunId = orchestrationRunId?.trim() || runId;
615
+ const sharedMemoryTaskId = orchestrationTaskId?.trim() || runId;
616
+ const availableCustomNames = options?.availableCustomSubagents ?? [];
617
+ const resolveCustom = (name) => {
618
+ if (!name || !options?.resolveCustomSubagent)
619
+ return undefined;
620
+ return options.resolveCustomSubagent(name);
621
+ };
622
+ let normalizedAgentName = agentName?.trim() || undefined;
623
+ let normalizedProfile = profile.trim().toLowerCase();
624
+ if (!normalizedProfile)
625
+ normalizedProfile = "full";
626
+ let customSubagent = resolveCustom(normalizedAgentName);
627
+ if (normalizedAgentName && !customSubagent) {
628
+ const available = availableCustomNames.length > 0 ? ` Available custom agents: ${availableCustomNames.join(", ")}.` : "";
629
+ throw new Error(`Unknown subagent: ${normalizedAgentName}.${available}`);
424
630
  }
425
- const effectiveProfile = customSubagent?.profile ?? profile;
631
+ // Recovery path: if model placed a custom agent name into `profile`, remap automatically.
632
+ if (!customSubagent) {
633
+ const profileAsAgent = resolveCustom(normalizedProfile);
634
+ if (profileAsAgent) {
635
+ customSubagent = profileAsAgent;
636
+ normalizedAgentName = profileAsAgent.name;
637
+ normalizedProfile = (profileAsAgent.profile ?? "full").trim().toLowerCase();
638
+ }
639
+ }
640
+ if (!toolsByProfile[normalizedProfile]) {
641
+ normalizedProfile = "full";
642
+ }
643
+ const effectiveProfile = customSubagent?.profile ?? normalizedProfile;
426
644
  let tools = customSubagent?.tools
427
645
  ? [...customSubagent.tools]
428
646
  : [...(toolsByProfile[effectiveProfile] ?? toolsByProfile.explore)];
@@ -431,10 +649,22 @@ export function createTaskTool(cwd, runner, options) {
431
649
  tools = tools.filter((tool) => !blocked.has(tool));
432
650
  }
433
651
  const delegationDepth = maxDelegationDepthFromEnv;
652
+ const requestedDelegateParallelHint = typeof delegateParallelHint === "number" && Number.isInteger(delegateParallelHint)
653
+ ? Math.max(1, Math.min(10, delegateParallelHint))
654
+ : undefined;
655
+ const autoDelegateParallelHint = requestedDelegateParallelHint === undefined
656
+ ? deriveAutoDelegateParallelHint(normalizedAgentName, description, prompt)
657
+ : undefined;
658
+ const effectiveDelegateParallelHint = requestedDelegateParallelHint ?? autoDelegateParallelHint;
659
+ const effectiveMaxDelegations = Math.max(0, Math.min(maxDelegationsPerTaskFromEnv, effectiveDelegateParallelHint ?? maxDelegationsPerTaskFromEnv));
660
+ const effectiveMaxDelegateParallel = Math.max(1, Math.min(maxDelegatedParallelFromEnv, effectiveDelegateParallelHint ?? maxDelegatedParallelFromEnv));
661
+ const minDelegationsPreferred = (effectiveDelegateParallelHint ?? 0) >= 2 && effectiveMaxDelegations >= 2
662
+ ? Math.min(2, effectiveMaxDelegations)
663
+ : 0;
434
664
  const baseSystemPrompt = customSubagent?.systemPrompt ??
435
665
  systemPromptByProfile[effectiveProfile] ??
436
666
  systemPromptByProfile.full;
437
- const systemPrompt = withDelegationPrompt(baseSystemPrompt, delegationDepth);
667
+ const systemPrompt = withDelegationPrompt(baseSystemPrompt, delegationDepth, effectiveMaxDelegations);
438
668
  const promptWithInstructions = customSubagent?.instructions && customSubagent.instructions.trim().length > 0
439
669
  ? `${customSubagent.instructions.trim()}\n\nUser task:\n${prompt}`
440
670
  : prompt;
@@ -503,11 +733,57 @@ export function createTaskTool(cwd, runner, options) {
503
733
  let releaseSlot;
504
734
  let releaseWriteLock;
505
735
  let releaseIsolation;
736
+ const explicitRootLockKey = lockKey?.trim();
506
737
  let subagentCwd = requestedSubagentCwd;
507
738
  let worktreePath;
508
739
  let runStats;
509
740
  try {
510
741
  throwIfAborted();
742
+ if (orchestrationRunId && orchestrationTaskId) {
743
+ try {
744
+ await waitForOrchestrationDependencies({
745
+ cwd,
746
+ runId: orchestrationRunId,
747
+ taskId: orchestrationTaskId,
748
+ signal: _signal,
749
+ onWaiting: (waiting) => {
750
+ emitProgress({
751
+ kind: "subagent_progress",
752
+ phase: "queued",
753
+ message: `waiting for dependencies: ${waiting}`,
754
+ cwd: requestedSubagentCwd,
755
+ activeTool: undefined,
756
+ });
757
+ },
758
+ });
759
+ }
760
+ catch (error) {
761
+ if (_signal?.aborted || isAbortError(error)) {
762
+ updateTrackedTaskStatus("cancelled");
763
+ throw new Error("Operation aborted");
764
+ }
765
+ const message = error instanceof Error ? error.message : String(error);
766
+ const cause = classifyFailureCause(message);
767
+ updateTrackedTaskStatus("error");
768
+ const details = {
769
+ profile: effectiveProfile,
770
+ description,
771
+ outputLength: 0,
772
+ cwd: requestedSubagentCwd,
773
+ agent: customSubagent?.name,
774
+ lockKey: lockKey?.trim() || undefined,
775
+ runId,
776
+ taskId: orchestrationTaskId,
777
+ model: effectiveModelOverride,
778
+ isolation: useWorktree ? "worktree" : "none",
779
+ worktreePath,
780
+ waitMs: Date.now() - queuedAt,
781
+ background: runInBackground,
782
+ failureCauses: { [cause]: 1 },
783
+ };
784
+ throw Object.assign(new Error(`Subagent failed: ${message}`), { details });
785
+ }
786
+ }
511
787
  const orchestrationSemaphore = orchestrationRunId && orchestrationTaskId
512
788
  ? getOrCreateOrchestrationSemaphore(cwd, orchestrationRunId)
513
789
  : undefined;
@@ -517,20 +793,12 @@ export function createTaskTool(cwd, runner, options) {
517
793
  }
518
794
  releaseSlot = await subagentSemaphore.acquire();
519
795
  throwIfAborted();
520
- if (orchestrationRunId && orchestrationTaskId) {
521
- updateTeamTaskStatus({
522
- cwd,
523
- runId: orchestrationRunId,
524
- taskId: orchestrationTaskId,
525
- status: "running",
526
- });
527
- }
796
+ updateTrackedTaskStatus("running");
528
797
  if (writeCapableProfiles.has(effectiveProfile)) {
529
- const explicitLockKey = lockKey?.trim();
530
798
  // Parallel orchestration should remain truly parallel by default.
531
799
  // Serialize write-capable agents only when an explicit lock_key is provided.
532
- if (explicitLockKey) {
533
- const lock = getOrCreateWriteLock(explicitLockKey);
800
+ if (explicitRootLockKey) {
801
+ const lock = getOrCreateWriteLock(explicitRootLockKey);
534
802
  releaseWriteLock = await lock.acquire();
535
803
  }
536
804
  }
@@ -552,18 +820,115 @@ export function createTaskTool(cwd, runner, options) {
552
820
  let delegatedTasks = 0;
553
821
  let delegatedSucceeded = 0;
554
822
  let delegatedFailed = 0;
823
+ let retrospectiveAttempts = 0;
824
+ let retrospectiveRecovered = 0;
555
825
  const delegationWarnings = [];
556
826
  const delegatedSections = [];
827
+ const failureCauses = {};
557
828
  const delegatedStats = {
558
829
  toolCallsStarted: 0,
559
830
  toolCallsCompleted: 0,
560
831
  assistantMessages: 0,
561
832
  };
833
+ const recordFailureCause = (cause) => {
834
+ failureCauses[cause] = (failureCauses[cause] ?? 0) + 1;
835
+ };
836
+ const rootSharedMemoryContext = {
837
+ rootCwd: cwd,
838
+ runId: sharedMemoryRunId,
839
+ taskId: sharedMemoryTaskId,
840
+ profile: effectiveProfile,
841
+ };
562
842
  try {
843
+ const runRootPass = async (runPrompt) => {
844
+ let emptyAttempt = 0;
845
+ let retrospectiveAttempt = 0;
846
+ let mergedStats;
847
+ let sessionId;
848
+ let promptForAttempt = runPrompt;
849
+ while (true) {
850
+ try {
851
+ const result = await runner({
852
+ systemPrompt,
853
+ tools,
854
+ prompt: promptForAttempt,
855
+ cwd: subagentCwd,
856
+ modelOverride: effectiveModelOverride,
857
+ sharedMemoryContext: rootSharedMemoryContext,
858
+ signal: _signal,
859
+ onProgress: (progress) => emitProgress(progress),
860
+ });
861
+ throwIfAborted();
862
+ let attemptOutput;
863
+ let attemptStats;
864
+ if (typeof result === "string") {
865
+ attemptOutput = result;
866
+ }
867
+ else {
868
+ attemptOutput = result.output;
869
+ attemptStats = result.stats;
870
+ sessionId = result.sessionId ?? sessionId;
871
+ }
872
+ mergedStats = mergeRunStats(mergedStats, attemptStats);
873
+ if (attemptOutput.trim().length > 0) {
874
+ if (retrospectiveAttempt > 0) {
875
+ retrospectiveRecovered += 1;
876
+ }
877
+ return {
878
+ output: attemptOutput,
879
+ sessionId,
880
+ stats: mergedStats,
881
+ };
882
+ }
883
+ if (emptyAttempt >= emptyOutputRetriesFromEnv) {
884
+ const totalAttempts = emptyAttempt + 1;
885
+ throw new Error(`Subagent returned empty output after ${totalAttempts} attempt${totalAttempts === 1 ? "" : "s"}.`);
886
+ }
887
+ emptyAttempt += 1;
888
+ emitProgress({
889
+ kind: "subagent_progress",
890
+ phase: "running",
891
+ message: `root subagent returned empty output; retry ${emptyAttempt}/${emptyOutputRetriesFromEnv}`,
892
+ cwd: subagentCwd,
893
+ activeTool: undefined,
894
+ });
895
+ }
896
+ catch (error) {
897
+ if (_signal?.aborted || isAbortError(error)) {
898
+ throw new Error("Operation aborted");
899
+ }
900
+ const message = error instanceof Error ? error.message : String(error);
901
+ const cause = classifyFailureCause(message);
902
+ recordFailureCause(cause);
903
+ const canRetryRetrospective = retrospectiveAttempt < retrospectiveRetriesFromEnv && isRetrospectiveRetryable(cause);
904
+ if (!canRetryRetrospective) {
905
+ throw Object.assign(new Error(message), { failureCause: cause });
906
+ }
907
+ retrospectiveAttempt += 1;
908
+ retrospectiveAttempts += 1;
909
+ const directive = buildRetrospectiveDirective({
910
+ cause,
911
+ errorMessage: message,
912
+ attempt: retrospectiveAttempt,
913
+ target: "root",
914
+ });
915
+ promptForAttempt = `${runPrompt}\n\n${directive}`;
916
+ emitProgress({
917
+ kind: "subagent_progress",
918
+ phase: "running",
919
+ message: `root retrospective retry ${retrospectiveAttempt}/${retrospectiveRetriesFromEnv} (${cause})`,
920
+ cwd: subagentCwd,
921
+ activeTool: undefined,
922
+ });
923
+ }
924
+ }
925
+ };
563
926
  const rootMeta = formatMetaCheckpoint(options?.getMetaMessages?.());
927
+ const rootSharedMemoryGuidance = buildSharedMemoryGuidance(sharedMemoryRunId, sharedMemoryTaskId);
928
+ const rootPromptBase = `${promptWithInstructions}\n\n${rootSharedMemoryGuidance}`;
564
929
  const rootPrompt = rootMeta.section && rootMeta.appliedCount > 0
565
- ? `${promptWithInstructions}\n\n${rootMeta.section}`
566
- : promptWithInstructions;
930
+ ? `${rootPromptBase}\n\n${rootMeta.section}`
931
+ : rootPromptBase;
567
932
  if (rootMeta.appliedCount > 0) {
568
933
  emitProgress({
569
934
  kind: "subagent_progress",
@@ -573,25 +938,39 @@ export function createTaskTool(cwd, runner, options) {
573
938
  activeTool: undefined,
574
939
  });
575
940
  }
576
- const result = await runner({
577
- systemPrompt,
578
- tools,
579
- prompt: rootPrompt,
580
- cwd: subagentCwd,
581
- modelOverride: effectiveModelOverride,
582
- signal: _signal,
583
- onProgress: (progress) => emitProgress(progress),
584
- });
585
- throwIfAborted();
586
- if (typeof result === "string") {
587
- output = result;
941
+ const firstPass = await runRootPass(rootPrompt);
942
+ output = firstPass.output;
943
+ subagentSessionId = firstPass.sessionId;
944
+ runStats = firstPass.stats;
945
+ let parsedDelegation = parseDelegationRequests(output, delegationDepth > 0 ? effectiveMaxDelegations : 0);
946
+ if (minDelegationsPreferred > 0 && parsedDelegation.requests.length < minDelegationsPreferred) {
947
+ emitProgress({
948
+ kind: "subagent_progress",
949
+ phase: "running",
950
+ message: `delegation preference unmet (${parsedDelegation.requests.length}/${minDelegationsPreferred}), retrying with stronger split guidance`,
951
+ cwd: subagentCwd,
952
+ activeTool: undefined,
953
+ });
954
+ const enforcedPrompt = [
955
+ rootPrompt,
956
+ "",
957
+ "[DELEGATION_ENFORCEMENT]",
958
+ `Prefer emitting at least ${minDelegationsPreferred} <delegate_task> blocks for independent sub-work when beneficial.`,
959
+ `Target parallel fan-out: up to ${effectiveMaxDelegateParallel}.`,
960
+ "If decomposition is not beneficial, you may keep single-agent execution and optionally output one line:",
961
+ "DELEGATION_IMPOSSIBLE: <reason>",
962
+ "[/DELEGATION_ENFORCEMENT]",
963
+ ].join("\n");
964
+ const secondPass = await runRootPass(enforcedPrompt);
965
+ output = secondPass.output;
966
+ subagentSessionId = secondPass.sessionId ?? subagentSessionId;
967
+ runStats = secondPass.stats ?? runStats;
968
+ parsedDelegation = parseDelegationRequests(output, delegationDepth > 0 ? effectiveMaxDelegations : 0);
588
969
  }
589
- else {
590
- output = result.output;
591
- subagentSessionId = result.sessionId;
592
- runStats = result.stats;
970
+ if (minDelegationsPreferred > 0 && parsedDelegation.requests.length < minDelegationsPreferred) {
971
+ const impossibleReason = output.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im)?.[1]?.trim() ?? "not provided";
972
+ delegationWarnings.push(`Delegation fallback: kept single-agent execution (preferred >=${minDelegationsPreferred} delegates, got ${parsedDelegation.requests.length}). Reason: ${impossibleReason}.`);
593
973
  }
594
- const parsedDelegation = parseDelegationRequests(output, delegationDepth > 0 ? maxDelegationsPerTaskFromEnv : 0);
595
974
  output = parsedDelegation.cleanedOutput;
596
975
  delegationWarnings.push(...parsedDelegation.warnings);
597
976
  const delegateTotal = parsedDelegation.requests.length;
@@ -619,7 +998,7 @@ export function createTaskTool(cwd, runner, options) {
619
998
  emitProgress({
620
999
  kind: "subagent_progress",
621
1000
  phase: "running",
622
- message: `delegation scheduler: ${delegateTotal} task(s), max parallel ${Math.min(delegateTotal, maxDelegatedParallelFromEnv)}`,
1001
+ message: `delegation scheduler: ${delegateTotal} task(s), max parallel ${Math.min(delegateTotal, effectiveMaxDelegateParallel)}`,
623
1002
  cwd: subagentCwd,
624
1003
  activeTool: undefined,
625
1004
  delegateTotal,
@@ -628,19 +1007,23 @@ export function createTaskTool(cwd, runner, options) {
628
1007
  }
629
1008
  const pendingIndices = new Set(Array.from({ length: delegateTotal }, (_v, i) => i));
630
1009
  const runningDelegates = new Map();
631
- const maxDelegateParallel = Math.max(1, Math.min(delegateTotal || 1, maxDelegatedParallelFromEnv));
1010
+ const maxDelegateParallel = Math.max(1, Math.min(delegateTotal || 1, effectiveMaxDelegateParallel));
632
1011
  const statusOf = (idx) => delegateItems[idx]?.status ?? "pending";
633
- const markDelegateFailed = (index, message, details) => {
1012
+ const formatDelegateTarget = (request) => {
1013
+ const agent = request.agent?.trim();
1014
+ return agent ? `${agent}/${request.profile}` : request.profile;
1015
+ };
1016
+ const markDelegateFailed = (index, message, details, cause) => {
634
1017
  const request = parsedDelegation.requests[index];
635
1018
  if (delegateItems[index]) {
636
1019
  delegateItems[index].status = "failed";
637
1020
  }
638
1021
  delegatedFailed += 1;
639
1022
  if (details) {
640
- delegationWarnings.push(details);
1023
+ delegationWarnings.push(cause ? `${details} [cause=${cause}]` : details);
641
1024
  }
642
- delegatedSections[index] =
643
- `#### ${index + 1}. ${request.description} (${request.profile})\nERROR: ${message}`;
1025
+ const causeLabel = cause ? ` [cause=${cause}]` : "";
1026
+ delegatedSections[index] = `#### ${index + 1}. ${request.description} (${formatDelegateTarget(request)})\nERROR${causeLabel}: ${message}`;
644
1027
  emitProgress({
645
1028
  kind: "subagent_progress",
646
1029
  phase: "running",
@@ -657,7 +1040,19 @@ export function createTaskTool(cwd, runner, options) {
657
1040
  const runDelegate = async (index) => {
658
1041
  throwIfAborted();
659
1042
  const request = parsedDelegation.requests[index];
660
- const childProfile = request.profile;
1043
+ const requestedChildAgent = request.agent?.trim() || undefined;
1044
+ const childCustomSubagent = resolveCustom(requestedChildAgent);
1045
+ if (requestedChildAgent && !childCustomSubagent) {
1046
+ delegationWarnings.push(`Delegated task "${request.description}" requested unknown agent "${requestedChildAgent}". Falling back to profile "${request.profile}".`);
1047
+ }
1048
+ const childProfileRaw = childCustomSubagent?.profile ?? request.profile;
1049
+ const normalizedChildProfile = childProfileRaw.trim().toLowerCase();
1050
+ const childProfile = normalizedChildProfile && toolsByProfile[normalizedChildProfile]
1051
+ ? normalizedChildProfile
1052
+ : "full";
1053
+ const childProfileLabel = childCustomSubagent?.name
1054
+ ? `${childCustomSubagent.name}/${childProfile}`
1055
+ : childProfile;
661
1056
  if (delegateItems[index]) {
662
1057
  delegateItems[index].status = "running";
663
1058
  }
@@ -670,23 +1065,34 @@ export function createTaskTool(cwd, runner, options) {
670
1065
  delegateIndex: index + 1,
671
1066
  delegateTotal,
672
1067
  delegateDescription: request.description,
673
- delegateProfile: childProfile,
1068
+ delegateProfile: childProfileLabel,
674
1069
  delegateItems,
675
1070
  });
676
- const childTools = [...(toolsByProfile[childProfile] ?? toolsByProfile.explore)];
677
- const childBaseSystemPrompt = systemPromptByProfile[childProfile] ?? systemPromptByProfile.full;
678
- const childSystemPrompt = withDelegationPrompt(childBaseSystemPrompt, Math.max(0, delegationDepth - 1));
679
- const requestedChildCwd = request.cwd ? path.resolve(subagentCwd, request.cwd) : subagentCwd;
1071
+ let childTools = childCustomSubagent?.tools
1072
+ ? [...childCustomSubagent.tools]
1073
+ : [...(toolsByProfile[childProfile] ?? toolsByProfile.explore)];
1074
+ if (childCustomSubagent?.disallowedTools?.length) {
1075
+ const blocked = new Set(childCustomSubagent.disallowedTools);
1076
+ childTools = childTools.filter((tool) => !blocked.has(tool));
1077
+ }
1078
+ const childBaseSystemPrompt = childCustomSubagent?.systemPrompt ??
1079
+ systemPromptByProfile[childProfile] ??
1080
+ systemPromptByProfile.full;
1081
+ const childSystemPrompt = withDelegationPrompt(childBaseSystemPrompt, Math.max(0, delegationDepth - 1), effectiveMaxDelegations);
1082
+ const requestedChildCwd = request.cwd
1083
+ ? path.resolve(subagentCwd, request.cwd)
1084
+ : childCustomSubagent?.cwd ?? subagentCwd;
680
1085
  if (!existsSync(requestedChildCwd) || !statSync(requestedChildCwd).isDirectory()) {
681
- markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} skipped: missing cwd`, `Delegated task "${request.description}" skipped: cwd does not exist (${requestedChildCwd}).`);
1086
+ recordFailureCause("dependency_env");
1087
+ markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} skipped: missing cwd`, `Delegated task "${request.description}" skipped: cwd does not exist (${requestedChildCwd}).`, "dependency_env");
682
1088
  return;
683
1089
  }
684
1090
  let childReleaseLock;
685
1091
  let childReleaseIsolation;
1092
+ const explicitChildLock = request.lockKey?.trim();
686
1093
  let childCwd = requestedChildCwd;
687
1094
  try {
688
1095
  throwIfAborted();
689
- const explicitChildLock = request.lockKey?.trim();
690
1096
  if (writeCapableProfiles.has(childProfile) && explicitChildLock) {
691
1097
  const lock = getOrCreateWriteLock(explicitChildLock);
692
1098
  childReleaseLock = await lock.acquire();
@@ -698,9 +1104,14 @@ export function createTaskTool(cwd, runner, options) {
698
1104
  childReleaseIsolation = isolated.cleanup;
699
1105
  }
700
1106
  const delegateMeta = formatMetaCheckpoint(options?.getMetaMessages?.());
701
- const delegatePrompt = delegateMeta.section && delegateMeta.appliedCount > 0
702
- ? `${request.prompt}\n\n${delegateMeta.section}`
1107
+ const childPromptWithInstructions = childCustomSubagent?.instructions && childCustomSubagent.instructions.trim().length > 0
1108
+ ? `${childCustomSubagent.instructions.trim()}\n\nUser task:\n${request.prompt}`
703
1109
  : request.prompt;
1110
+ const delegateSharedMemoryGuidance = buildSharedMemoryGuidance(sharedMemoryRunId, sharedMemoryTaskId);
1111
+ const delegatePromptBase = `${childPromptWithInstructions}\n\n${delegateSharedMemoryGuidance}`;
1112
+ const delegatePrompt = delegateMeta.section && delegateMeta.appliedCount > 0
1113
+ ? `${delegatePromptBase}\n\n${delegateMeta.section}`
1114
+ : delegatePromptBase;
704
1115
  if (delegateMeta.appliedCount > 0) {
705
1116
  emitProgress({
706
1117
  kind: "subagent_progress",
@@ -711,41 +1122,118 @@ export function createTaskTool(cwd, runner, options) {
711
1122
  delegateIndex: index + 1,
712
1123
  delegateTotal,
713
1124
  delegateDescription: request.description,
714
- delegateProfile: childProfile,
1125
+ delegateProfile: childProfileLabel,
715
1126
  delegateItems,
716
1127
  });
717
1128
  }
718
- const childResult = await runner({
719
- systemPrompt: childSystemPrompt,
720
- tools: childTools,
721
- prompt: delegatePrompt,
722
- cwd: childCwd,
723
- modelOverride: request.model,
724
- signal: _signal,
725
- onProgress: (progress) => {
1129
+ const childModelOverride = request.model?.trim() || childCustomSubagent?.model?.trim() || undefined;
1130
+ const childSharedMemoryContext = {
1131
+ rootCwd: cwd,
1132
+ runId: sharedMemoryRunId,
1133
+ taskId: sharedMemoryTaskId,
1134
+ delegateId: String(index + 1),
1135
+ profile: childProfile,
1136
+ };
1137
+ let childOutput = "";
1138
+ let childStats;
1139
+ let childEmptyAttempt = 0;
1140
+ let childRetrospectiveAttempt = 0;
1141
+ let childPromptForAttempt = delegatePrompt;
1142
+ while (true) {
1143
+ try {
1144
+ const childResult = await runner({
1145
+ systemPrompt: childSystemPrompt,
1146
+ tools: childTools,
1147
+ prompt: childPromptForAttempt,
1148
+ cwd: childCwd,
1149
+ modelOverride: childModelOverride,
1150
+ sharedMemoryContext: childSharedMemoryContext,
1151
+ signal: _signal,
1152
+ onProgress: (progress) => {
1153
+ emitProgress({
1154
+ kind: "subagent_progress",
1155
+ phase: "running",
1156
+ message: `delegate ${index + 1}/${delegateTotal}: ${progress.message}`,
1157
+ cwd: progress.cwd ?? childCwd,
1158
+ activeTool: progress.activeTool,
1159
+ delegateIndex: index + 1,
1160
+ delegateTotal,
1161
+ delegateDescription: request.description,
1162
+ delegateProfile: childProfileLabel,
1163
+ delegateItems,
1164
+ });
1165
+ },
1166
+ });
1167
+ throwIfAborted();
1168
+ let attemptOutput;
1169
+ let attemptStats;
1170
+ if (typeof childResult === "string") {
1171
+ attemptOutput = childResult;
1172
+ }
1173
+ else {
1174
+ attemptOutput = childResult.output;
1175
+ attemptStats = childResult.stats;
1176
+ }
1177
+ childStats = mergeRunStats(childStats, attemptStats);
1178
+ if (attemptOutput.trim().length > 0) {
1179
+ childOutput = attemptOutput;
1180
+ if (childRetrospectiveAttempt > 0) {
1181
+ retrospectiveRecovered += 1;
1182
+ }
1183
+ break;
1184
+ }
1185
+ if (childEmptyAttempt >= emptyOutputRetriesFromEnv) {
1186
+ const totalAttempts = childEmptyAttempt + 1;
1187
+ throw new Error(`delegate ${index + 1}/${delegateTotal} returned empty output after ${totalAttempts} attempt${totalAttempts === 1 ? "" : "s"}.`);
1188
+ }
1189
+ childEmptyAttempt += 1;
726
1190
  emitProgress({
727
1191
  kind: "subagent_progress",
728
1192
  phase: "running",
729
- message: `delegate ${index + 1}/${delegateTotal}: ${progress.message}`,
730
- cwd: progress.cwd ?? childCwd,
731
- activeTool: progress.activeTool,
1193
+ message: `delegate ${index + 1}/${delegateTotal}: empty output, retry ${childEmptyAttempt}/${emptyOutputRetriesFromEnv}`,
1194
+ cwd: childCwd,
1195
+ activeTool: undefined,
732
1196
  delegateIndex: index + 1,
733
1197
  delegateTotal,
734
1198
  delegateDescription: request.description,
735
- delegateProfile: childProfile,
1199
+ delegateProfile: childProfileLabel,
736
1200
  delegateItems,
737
1201
  });
738
- },
739
- });
740
- throwIfAborted();
741
- let childOutput;
742
- let childStats;
743
- if (typeof childResult === "string") {
744
- childOutput = childResult;
745
- }
746
- else {
747
- childOutput = childResult.output;
748
- childStats = childResult.stats;
1202
+ }
1203
+ catch (error) {
1204
+ if (_signal?.aborted || isAbortError(error)) {
1205
+ throw new Error("Operation aborted");
1206
+ }
1207
+ const message = error instanceof Error ? error.message : String(error);
1208
+ const cause = classifyFailureCause(message);
1209
+ recordFailureCause(cause);
1210
+ const canRetryRetrospective = childRetrospectiveAttempt < retrospectiveRetriesFromEnv &&
1211
+ isRetrospectiveRetryable(cause);
1212
+ if (!canRetryRetrospective) {
1213
+ throw Object.assign(new Error(message), { failureCause: cause });
1214
+ }
1215
+ childRetrospectiveAttempt += 1;
1216
+ retrospectiveAttempts += 1;
1217
+ const directive = buildRetrospectiveDirective({
1218
+ cause,
1219
+ errorMessage: message,
1220
+ attempt: childRetrospectiveAttempt,
1221
+ target: "delegate",
1222
+ });
1223
+ childPromptForAttempt = `${delegatePrompt}\n\n${directive}`;
1224
+ emitProgress({
1225
+ kind: "subagent_progress",
1226
+ phase: "running",
1227
+ message: `delegate ${index + 1}/${delegateTotal}: retrospective retry ${childRetrospectiveAttempt}/${retrospectiveRetriesFromEnv} (${cause})`,
1228
+ cwd: childCwd,
1229
+ activeTool: undefined,
1230
+ delegateIndex: index + 1,
1231
+ delegateTotal,
1232
+ delegateDescription: request.description,
1233
+ delegateProfile: childProfileLabel,
1234
+ delegateItems,
1235
+ });
1236
+ }
749
1237
  }
750
1238
  const parsedChildDelegation = parseDelegationRequests(childOutput, 0);
751
1239
  childOutput = parsedChildDelegation.cleanedOutput;
@@ -762,7 +1250,7 @@ export function createTaskTool(cwd, runner, options) {
762
1250
  ? `${normalizedChildOutput.slice(0, Math.max(1, maxDelegatedOutputCharsFromEnv - 3))}...`
763
1251
  : normalizedChildOutput;
764
1252
  delegatedSections[index] =
765
- `#### ${index + 1}. ${request.description} (${childProfile})\n${childOutputExcerpt}`;
1253
+ `#### ${index + 1}. ${request.description} (${childProfileLabel})\n${childOutputExcerpt}`;
766
1254
  emitProgress({
767
1255
  kind: "subagent_progress",
768
1256
  phase: "running",
@@ -772,7 +1260,7 @@ export function createTaskTool(cwd, runner, options) {
772
1260
  delegateIndex: index + 1,
773
1261
  delegateTotal,
774
1262
  delegateDescription: request.description,
775
- delegateProfile: childProfile,
1263
+ delegateProfile: childProfileLabel,
776
1264
  delegateItems,
777
1265
  });
778
1266
  }
@@ -781,11 +1269,18 @@ export function createTaskTool(cwd, runner, options) {
781
1269
  if (_signal?.aborted || isAbortError(error)) {
782
1270
  throw new Error("Operation aborted");
783
1271
  }
784
- markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} failed`, message);
1272
+ const classified = error && typeof error === "object" && "failureCause" in error
1273
+ ? error.failureCause
1274
+ : classifyFailureCause(message);
1275
+ if (!(error && typeof error === "object" && "failureCause" in error)) {
1276
+ recordFailureCause(classified);
1277
+ }
1278
+ markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} failed`, message, classified);
785
1279
  }
786
1280
  finally {
787
1281
  childReleaseIsolation?.();
788
1282
  childReleaseLock?.();
1283
+ cleanupWriteLock(explicitChildLock);
789
1284
  }
790
1285
  };
791
1286
  const resolveBlockedByFailedDependencies = () => {
@@ -798,7 +1293,8 @@ export function createTaskTool(cwd, runner, options) {
798
1293
  if (!failedDep)
799
1294
  continue;
800
1295
  pendingIndices.delete(index);
801
- markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} skipped: dependency ${failedDep} failed`, `Delegated task ${index + 1} skipped because dependency ${failedDep} failed.`);
1296
+ recordFailureCause("logic_error");
1297
+ markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} skipped: dependency ${failedDep} failed`, `Delegated task ${index + 1} skipped because dependency ${failedDep} failed.`, "logic_error");
802
1298
  changed = true;
803
1299
  }
804
1300
  return changed;
@@ -836,7 +1332,8 @@ export function createTaskTool(cwd, runner, options) {
836
1332
  for (const index of Array.from(pendingIndices)) {
837
1333
  pendingIndices.delete(index);
838
1334
  const deps = normalizedDependsOn[index] ?? [];
839
- markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} blocked: unresolved depends_on`, `Delegated task ${index + 1} blocked by unresolved dependencies: ${deps.join(", ") || "unknown"}.`);
1335
+ recordFailureCause("logic_error");
1336
+ markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} blocked: unresolved depends_on`, `Delegated task ${index + 1} blocked by unresolved dependencies: ${deps.join(", ") || "unknown"}.`, "logic_error");
840
1337
  }
841
1338
  }
842
1339
  break;
@@ -847,8 +1344,45 @@ export function createTaskTool(cwd, runner, options) {
847
1344
  catch (error) {
848
1345
  const message = error instanceof Error ? error.message : String(error);
849
1346
  if (_signal?.aborted || isAbortError(error)) {
850
- throw new Error("Operation aborted");
1347
+ recordFailureCause("aborted");
1348
+ const hasFailureCauses = Object.keys(failureCauses).length > 0;
1349
+ const details = {
1350
+ profile: effectiveProfile,
1351
+ description,
1352
+ outputLength: 0,
1353
+ cwd: subagentCwd,
1354
+ agent: customSubagent?.name,
1355
+ lockKey: lockKey?.trim() || undefined,
1356
+ runId,
1357
+ taskId: orchestrationTaskId,
1358
+ model: effectiveModelOverride,
1359
+ isolation: useWorktree ? "worktree" : "none",
1360
+ worktreePath,
1361
+ waitMs: Date.now() - queuedAt,
1362
+ background: runInBackground,
1363
+ toolCallsStarted: runStats?.toolCallsStarted ?? latestProgress?.toolCallsStarted,
1364
+ toolCallsCompleted: runStats?.toolCallsCompleted ?? latestProgress?.toolCallsCompleted,
1365
+ assistantMessages: runStats?.assistantMessages ?? latestProgress?.assistantMessages,
1366
+ delegatedTasks: delegatedTasks > 0 ? delegatedTasks : undefined,
1367
+ delegatedSucceeded: delegatedTasks > 0 ? delegatedSucceeded : undefined,
1368
+ delegatedFailed: delegatedTasks > 0 ? delegatedFailed : undefined,
1369
+ retrospectiveAttempts: retrospectiveAttempts > 0 ? retrospectiveAttempts : undefined,
1370
+ retrospectiveRecovered: retrospectiveRecovered > 0 ? retrospectiveRecovered : undefined,
1371
+ failureCauses: hasFailureCauses ? { ...failureCauses } : undefined,
1372
+ };
1373
+ updateTrackedTaskStatus("cancelled");
1374
+ throw Object.assign(new Error("Operation aborted"), {
1375
+ details,
1376
+ failureCause: "aborted",
1377
+ });
1378
+ }
1379
+ const classified = error && typeof error === "object" && "failureCause" in error
1380
+ ? error.failureCause
1381
+ : classifyFailureCause(message);
1382
+ if (!(error && typeof error === "object" && "failureCause" in error)) {
1383
+ recordFailureCause(classified);
851
1384
  }
1385
+ const hasFailureCauses = Object.keys(failureCauses).length > 0;
852
1386
  const details = {
853
1387
  profile: effectiveProfile,
854
1388
  description,
@@ -869,15 +1403,11 @@ export function createTaskTool(cwd, runner, options) {
869
1403
  delegatedTasks: delegatedTasks > 0 ? delegatedTasks : undefined,
870
1404
  delegatedSucceeded: delegatedTasks > 0 ? delegatedSucceeded : undefined,
871
1405
  delegatedFailed: delegatedTasks > 0 ? delegatedFailed : undefined,
1406
+ retrospectiveAttempts: retrospectiveAttempts > 0 ? retrospectiveAttempts : undefined,
1407
+ retrospectiveRecovered: retrospectiveRecovered > 0 ? retrospectiveRecovered : undefined,
1408
+ failureCauses: hasFailureCauses ? { ...failureCauses } : undefined,
872
1409
  };
873
- if (orchestrationRunId && orchestrationTaskId) {
874
- updateTeamTaskStatus({
875
- cwd,
876
- runId: orchestrationRunId,
877
- taskId: orchestrationTaskId,
878
- status: "error",
879
- });
880
- }
1410
+ updateTrackedTaskStatus("error");
881
1411
  throw Object.assign(new Error(`Subagent failed: ${message}`), { details });
882
1412
  }
883
1413
  const normalizedOutput = output.trim().length > 0 ? output.trim() : "(Subagent completed with no output)";
@@ -890,6 +1420,15 @@ export function createTaskTool(cwd, runner, options) {
890
1420
  if (delegationWarnings.length > 0) {
891
1421
  finalSections.push(`### Delegation Notes\n${delegationWarnings.map((w) => `- ${w}`).join("\n")}`);
892
1422
  }
1423
+ const failureCauseSummary = formatFailureCauseCounts(failureCauses);
1424
+ if (retrospectiveAttempts > 0 || failureCauseSummary) {
1425
+ finalSections.push([
1426
+ "### Retrospective",
1427
+ `- attempts: ${retrospectiveAttempts}`,
1428
+ `- recovered: ${retrospectiveRecovered}`,
1429
+ `- failure_causes: ${failureCauseSummary || "none"}`,
1430
+ ].join("\n"));
1431
+ }
893
1432
  const text = finalSections.join("\n\n");
894
1433
  emitProgress({
895
1434
  kind: "subagent_progress",
@@ -918,6 +1457,7 @@ export function createTaskTool(cwd, runner, options) {
918
1457
  isolation: useWorktree ? "worktree" : "none",
919
1458
  worktreePath,
920
1459
  });
1460
+ const hasFailureCauses = Object.keys(failureCauses).length > 0;
921
1461
  const details = {
922
1462
  profile: effectiveProfile,
923
1463
  description,
@@ -955,22 +1495,22 @@ export function createTaskTool(cwd, runner, options) {
955
1495
  delegatedTasks: delegatedTasks > 0 ? delegatedTasks : undefined,
956
1496
  delegatedSucceeded: delegatedTasks > 0 ? delegatedSucceeded : undefined,
957
1497
  delegatedFailed: delegatedTasks > 0 ? delegatedFailed : undefined,
1498
+ retrospectiveAttempts: retrospectiveAttempts > 0 ? retrospectiveAttempts : undefined,
1499
+ retrospectiveRecovered: retrospectiveRecovered > 0 ? retrospectiveRecovered : undefined,
1500
+ failureCauses: hasFailureCauses ? { ...failureCauses } : undefined,
958
1501
  };
959
- if (orchestrationRunId && orchestrationTaskId) {
960
- updateTeamTaskStatus({
961
- cwd,
962
- runId: orchestrationRunId,
963
- taskId: orchestrationTaskId,
964
- status: "done",
965
- });
966
- }
1502
+ updateTrackedTaskStatus("done");
967
1503
  return { text, details };
968
1504
  }
969
1505
  finally {
970
1506
  releaseIsolation?.();
971
1507
  releaseWriteLock?.();
1508
+ cleanupWriteLock(explicitRootLockKey);
972
1509
  releaseSlot?.();
973
1510
  releaseRunSlot?.();
1511
+ if (orchestrationRunId) {
1512
+ cleanupOrchestrationSemaphore(cwd, orchestrationRunId);
1513
+ }
974
1514
  }
975
1515
  };
976
1516
  if (runInBackground) {
@@ -1013,9 +1553,10 @@ export function createTaskTool(cwd, runner, options) {
1013
1553
  });
1014
1554
  }
1015
1555
  catch (error) {
1556
+ const aborted = isAbortError(error);
1016
1557
  writeBackgroundRunStatus(cwd, {
1017
1558
  runId,
1018
- status: "error",
1559
+ status: aborted ? "cancelled" : "error",
1019
1560
  createdAt: now,
1020
1561
  finishedAt: new Date().toISOString(),
1021
1562
  description,