iosm-cli 0.2.4 → 0.2.5

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 (65) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +23 -17
  3. package/dist/core/agent-profiles.d.ts +1 -0
  4. package/dist/core/agent-profiles.d.ts.map +1 -1
  5. package/dist/core/agent-profiles.js +10 -6
  6. package/dist/core/agent-profiles.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +1 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +6 -2
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/agent-teams.d.ts.map +1 -1
  12. package/dist/core/agent-teams.js +90 -19
  13. package/dist/core/agent-teams.js.map +1 -1
  14. package/dist/core/footer-data-provider.d.ts +6 -1
  15. package/dist/core/footer-data-provider.d.ts.map +1 -1
  16. package/dist/core/footer-data-provider.js +9 -0
  17. package/dist/core/footer-data-provider.js.map +1 -1
  18. package/dist/core/parallel-task-agent.d.ts +23 -1
  19. package/dist/core/parallel-task-agent.d.ts.map +1 -1
  20. package/dist/core/parallel-task-agent.js +110 -20
  21. package/dist/core/parallel-task-agent.js.map +1 -1
  22. package/dist/core/shared-memory.d.ts +2 -2
  23. package/dist/core/shared-memory.d.ts.map +1 -1
  24. package/dist/core/shared-memory.js +220 -91
  25. package/dist/core/shared-memory.js.map +1 -1
  26. package/dist/core/singular.d.ts.map +1 -1
  27. package/dist/core/singular.js +3 -1
  28. package/dist/core/singular.js.map +1 -1
  29. package/dist/core/subagents.d.ts +1 -1
  30. package/dist/core/subagents.d.ts.map +1 -1
  31. package/dist/core/subagents.js +11 -3
  32. package/dist/core/subagents.js.map +1 -1
  33. package/dist/core/swarm/planner.d.ts.map +1 -1
  34. package/dist/core/swarm/planner.js +200 -12
  35. package/dist/core/swarm/planner.js.map +1 -1
  36. package/dist/core/swarm/scheduler.d.ts +2 -0
  37. package/dist/core/swarm/scheduler.d.ts.map +1 -1
  38. package/dist/core/swarm/scheduler.js +87 -6
  39. package/dist/core/swarm/scheduler.js.map +1 -1
  40. package/dist/core/system-prompt.d.ts.map +1 -1
  41. package/dist/core/system-prompt.js +1 -0
  42. package/dist/core/system-prompt.js.map +1 -1
  43. package/dist/core/tools/ast-grep.d.ts.map +1 -1
  44. package/dist/core/tools/ast-grep.js +2 -0
  45. package/dist/core/tools/ast-grep.js.map +1 -1
  46. package/dist/core/tools/shared-memory.d.ts.map +1 -1
  47. package/dist/core/tools/shared-memory.js +34 -6
  48. package/dist/core/tools/shared-memory.js.map +1 -1
  49. package/dist/core/tools/task.d.ts.map +1 -1
  50. package/dist/core/tools/task.js +464 -73
  51. package/dist/core/tools/task.js.map +1 -1
  52. package/dist/core/tools/yq.d.ts.map +1 -1
  53. package/dist/core/tools/yq.js +2 -0
  54. package/dist/core/tools/yq.js.map +1 -1
  55. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/footer.js +2 -1
  57. package/dist/modes/interactive/components/footer.js.map +1 -1
  58. package/dist/modes/interactive/interactive-mode.d.ts +13 -0
  59. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  60. package/dist/modes/interactive/interactive-mode.js +756 -74
  61. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  62. package/docs/cli-reference.md +4 -0
  63. package/docs/interactive-mode.md +2 -0
  64. package/docs/orchestration-and-subagents.md +5 -0
  65. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { Type } from "@sinclair/typebox";
5
5
  import { getTeamRun, updateTeamTaskStatus } from "../agent-teams.js";
6
6
  import { buildRetrospectiveDirective, classifyFailureCause, formatFailureCauseCounts, isRetrospectiveRetryable, } from "../failure-retrospective.js";
7
7
  import { MAX_ORCHESTRATION_AGENTS, MAX_ORCHESTRATION_PARALLEL, MAX_SUBAGENT_DELEGATE_PARALLEL, MAX_SUBAGENT_DELEGATION_DEPTH, MAX_SUBAGENT_DELEGATIONS_PER_TASK, } from "../orchestration-limits.js";
8
+ import { AGENT_PROFILES, isReadOnlyProfileName, isValidProfileName, } from "../agent-profiles.js";
8
9
  const taskSchema = Type.Object({
9
10
  description: Type.Optional(Type.String({
10
11
  description: "Optional short 3-5 word description of what the subagent will do. If omitted, it is derived from prompt.",
@@ -22,7 +23,7 @@ const taskSchema = Type.Object({
22
23
  description: "Optional custom subagent name loaded from .iosm/agents or global agents directory.",
23
24
  })),
24
25
  profile: Type.Optional(Type.String({
25
- description: "Optional subagent capability profile. Defaults to full when omitted. Recommended values: explore, plan, iosm, meta, iosm_analyst, iosm_verifier, cycle_planner, full. For custom agents, pass the agent name via `agent`, not `profile`.",
26
+ description: "Optional subagent capability profile. Defaults to the current host profile when omitted (or full if host profile is unavailable). Recommended values: explore, plan, iosm, meta, iosm_analyst, iosm_verifier, cycle_planner, full. For custom agents, pass the agent name via `agent`, not `profile`.",
26
27
  })),
27
28
  cwd: Type.Optional(Type.String({
28
29
  description: "Optional working directory for this subagent. Relative paths are resolved from the current workspace.",
@@ -51,29 +52,26 @@ const taskSchema = Type.Object({
51
52
  description: "Optional hint for intra-task delegation fan-out. Higher value allows more delegated subtasks to run in parallel inside a single task execution.",
52
53
  })),
53
54
  });
54
- /** Tool names available per profile */
55
- const toolsByProfile = {
56
- explore: ["read", "grep", "find", "ls"],
57
- plan: ["read", "bash", "grep", "find", "ls"],
58
- iosm: ["read", "bash", "edit", "write", "grep", "find", "ls"],
59
- meta: ["read", "bash", "edit", "write", "grep", "find", "ls"],
60
- iosm_analyst: ["read", "bash", "grep", "find", "ls"],
61
- iosm_verifier: ["read", "bash", "write"],
62
- cycle_planner: ["read", "bash", "write"],
63
- full: ["read", "bash", "edit", "write", "grep", "find", "ls"],
64
- };
55
+ /** Tool names available per profile (kept in sync with AGENT_PROFILES). */
56
+ const toolsByProfile = Object.values(AGENT_PROFILES).reduce((acc, profile) => {
57
+ acc[profile.name] = [...profile.tools];
58
+ return acc;
59
+ }, {});
65
60
  /** System prompt injected per profile */
66
61
  const systemPromptByProfile = {
67
62
  explore: "You are a fast read-only codebase explorer. Answer concisely. Never write or edit files.",
68
63
  plan: "You are a technical architect. Analyze the codebase and produce a clear implementation plan. Do not write or edit files.",
69
64
  iosm: "You are an IOSM execution agent. Use IOSM methodology and keep IOSM artifacts synchronized with implementation.",
70
- meta: "You are a meta orchestration agent. Your main job is to maximize safe parallel execution through delegates, not to personally do most of the implementation. Start with bounded read-only recon, then form a concrete execution graph: subtasks, delegate subtasks, dependencies, lock domains, and verification steps. The parent agent remains responsible for orchestration and synthesis, so decompose work aggressively instead of collapsing complex work into one worker. For any non-trivial task, orchestration is required: after recon, launch multiple focused delegates instead of continuing manual implementation in the parent agent, avoid direct write/edit work in the parent agent before delegation unless the task is clearly trivial, and do not hand the whole task to one specialist child when independent workstreams exist. If a delegated workstream still contains multiple independent slices, split it again with nested <delegate_task> blocks. Default to aggressive safe parallelism. If the user requested a specific degree of parallelism, honor it when feasible or explain the exact blocker. When delegation is not used for non-trivial work, explain why in one line and include DELEGATION_IMPOSSIBLE. Enforce test verification for code changes, complete only after all delegated branches are resolved, and explicitly justify any no-code path where tests are skipped.",
65
+ meta: "You are a meta orchestration agent. Your main job is to maximize safe parallel execution through delegates, not to personally do most of the implementation. Start with bounded read-only recon, then form a concrete execution graph: subtasks, delegate subtasks, dependencies, lock domains, and verification steps. The parent agent remains responsible for orchestration and synthesis, so decompose work aggressively instead of collapsing complex work into one worker. For any non-trivial task, orchestration is required: after recon, launch multiple focused delegates instead of continuing manual implementation in the parent agent, avoid direct write/edit work in the parent agent before delegation unless the task is clearly trivial, and do not hand the whole task to one specialist child when independent workstreams exist. If a delegated workstream still contains multiple independent slices, split it again with nested <delegate_task> blocks. Default to aggressive safe parallelism. If the user requested a specific degree of parallelism, honor it when feasible or explain the exact blocker. Use shared_memory as the default coordination channel between delegates: use stable namespaced keys, prefer read-before-write, and use CAS (if_version) for contested updates; reserve append mode for timeline/log keys. When delegation is not used for non-trivial work, explain why in one line and include DELEGATION_IMPOSSIBLE. Enforce test verification for code changes, complete only after all delegated branches are resolved, and explicitly justify any no-code path where tests are skipped.",
71
66
  iosm_analyst: "You are an IOSM metrics analyst. Analyze .iosm/ artifacts and codebase metrics. Be precise and evidence-based.",
72
67
  iosm_verifier: "You are an IOSM verifier. Validate checks and update only required IOSM artifacts with deterministic reasoning.",
73
68
  cycle_planner: "You are an IOSM cycle planner. Propose and align cycle goals with measurable outcomes and concrete risks.",
74
69
  full: "You are a software engineering agent. Execute the task end-to-end.",
75
70
  };
76
- const writeCapableProfiles = new Set(["full", "meta", "iosm_verifier", "cycle_planner"]);
71
+ const writeCapableTools = new Set(["bash", "edit", "write"]);
72
+ const backgroundUnsafeTools = new Set(writeCapableTools);
73
+ const writeCapableProfiles = new Set(Object.keys(toolsByProfile).filter((profileName) => toolsByProfile[profileName].some((tool) => writeCapableTools.has(tool))));
74
+ const backgroundSafeProfiles = Object.keys(toolsByProfile).filter((profileName) => toolsByProfile[profileName].every((tool) => !backgroundUnsafeTools.has(tool)));
77
75
  const delegationTagName = "delegate_task";
78
76
  class Semaphore {
79
77
  constructor(limit) {
@@ -132,7 +130,7 @@ class Mutex {
132
130
  }
133
131
  const maxParallelFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_PARALLEL, MAX_ORCHESTRATION_PARALLEL, 1, MAX_ORCHESTRATION_PARALLEL);
134
132
  const subagentSemaphore = new Semaphore(maxParallelFromEnv);
135
- const maxDelegationDepthFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATION_DEPTH, 1, 0, MAX_SUBAGENT_DELEGATION_DEPTH);
133
+ const maxDelegationDepthFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATION_DEPTH, 2, 0, MAX_SUBAGENT_DELEGATION_DEPTH);
136
134
  const maxDelegationsPerTaskFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATIONS_PER_TASK, MAX_SUBAGENT_DELEGATIONS_PER_TASK, 0, MAX_SUBAGENT_DELEGATIONS_PER_TASK);
137
135
  const maxDelegatedParallelFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATE_PARALLEL, MAX_SUBAGENT_DELEGATE_PARALLEL, 1, MAX_SUBAGENT_DELEGATE_PARALLEL);
138
136
  const emptyOutputRetriesFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_EMPTY_OUTPUT_RETRIES, 1, 0, 2);
@@ -179,6 +177,9 @@ function deriveAutoDelegateParallelHint(profile, agentName, hostProfile, descrip
179
177
  const fileLikeMatches = normalized.match(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,8}\b/g) ?? [];
180
178
  const listMarkers = text.match(/(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/g)?.length ?? 0;
181
179
  const hasCodeBlock = text.includes("```");
180
+ const actionTokenMatches = normalized.match(/\b(?:audit|security|auth|rbac|sqli|sql|injection|fix|implement|refactor|migrat|harden|verify|test|scan|orchestrate|parallel|delegate|bug|vulnerab)\w*/gi) ?? [];
181
+ const strongActionSignal = new Set(actionTokenMatches.map((token) => token.toLowerCase())).size >= 2;
182
+ const metaOrchestratorContext = isMetaProfile || isMetaHost;
182
183
  let score = 0;
183
184
  if (words >= 40) {
184
185
  score += 2;
@@ -196,15 +197,32 @@ function deriveAutoDelegateParallelHint(profile, agentName, hostProfile, descrip
196
197
  score += 1;
197
198
  }
198
199
  const referenceCount = pathLikeMatches.length + fileLikeMatches.length;
200
+ const metaNonTrivialSignal = words >= 12 ||
201
+ clauses >= 3 ||
202
+ listMarkers >= 1 ||
203
+ referenceCount >= 1 ||
204
+ hasCodeBlock ||
205
+ (strongActionSignal && words >= 4);
199
206
  if (referenceCount >= 3 || (referenceCount >= 1 && words >= 20)) {
200
207
  score += 1;
201
208
  }
202
209
  if (hasCodeBlock) {
203
210
  score += 1;
204
211
  }
205
- if ((isMetaProfile || isMetaHost) && score > 0) {
206
- // Meta profile is intentionally parallel-biased for non-trivial work.
207
- score += 1;
212
+ if (metaOrchestratorContext) {
213
+ // In meta orchestration, require delegation pressure for non-trivial prompts
214
+ // even when lexical scoring is still low.
215
+ if (score === 0) {
216
+ if (strongActionSignal && words >= 4) {
217
+ score = 1;
218
+ }
219
+ else if (metaNonTrivialSignal) {
220
+ score = 2;
221
+ }
222
+ }
223
+ else if (score > 0) {
224
+ score += 1;
225
+ }
208
226
  }
209
227
  if (score >= 6)
210
228
  return 10;
@@ -320,6 +338,9 @@ function buildDelegationProtocolPrompt(depthRemaining, maxDelegations, minDelega
320
338
  `Keep a brief coordinator note outside the blocks, but do not collapse the full workload into one monolithic answer.`,
321
339
  `If safe decomposition is truly impossible, output exactly one line: DELEGATION_IMPOSSIBLE: <precise reason>.`,
322
340
  `When shared_memory tools are available, exchange intermediate state through shared_memory_write/shared_memory_read instead of repeating large context.`,
341
+ `Shared-memory protocol: use stable namespaced keys (findings/<stream>, plan/<stream>, risks/<stream>).`,
342
+ `Use scope=run for cross-stream coordination, scope=task for local scratch state, read before overwrite, and use if_version for contested updates.`,
343
+ `Reserve mode=append for timeline/log keys only; avoid append on canonical state keys.`,
323
344
  ].join("\n");
324
345
  }
325
346
  return [
@@ -329,8 +350,155 @@ function buildDelegationProtocolPrompt(depthRemaining, maxDelegations, minDelega
329
350
  `</${delegationTagName}>`,
330
351
  `Only emit blocks when necessary. Keep normal analysis/answer text outside those blocks.`,
331
352
  `When shared_memory tools are available, exchange intermediate state through shared_memory_write/shared_memory_read instead of repeating large context.`,
353
+ `Shared-memory protocol: prefer namespaced keys and read-before-write discipline; use CAS (if_version) on shared state updates.`,
354
+ `Reserve mode=append for timeline/log keys only.`,
332
355
  ].join("\n");
333
356
  }
357
+ function truncateForDelegationContext(text, maxChars = 2200) {
358
+ const normalized = normalizeSpacing(text);
359
+ if (normalized.length <= maxChars)
360
+ return normalized;
361
+ return `${normalized.slice(0, Math.max(100, maxChars - 3)).trimEnd()}...`;
362
+ }
363
+ function extractDelegationWorkstreams(text, maxItems) {
364
+ if (maxItems <= 0)
365
+ return [];
366
+ const seen = new Set();
367
+ const pushUnique = (raw) => {
368
+ const cleaned = raw
369
+ .replace(/^[-*]\s+/, "")
370
+ .replace(/^\d+[.)]\s+/, "")
371
+ .replace(/\s+/g, " ")
372
+ .trim();
373
+ if (cleaned.length < 5)
374
+ return;
375
+ const key = cleaned.toLowerCase();
376
+ if (seen.has(key))
377
+ return;
378
+ seen.add(key);
379
+ };
380
+ for (const line of text.split("\n")) {
381
+ if (!/^\s*(?:[-*]|\d+[.)])\s+/.test(line))
382
+ continue;
383
+ pushUnique(line);
384
+ if (seen.size >= maxItems)
385
+ break;
386
+ }
387
+ if (seen.size < maxItems) {
388
+ const fragments = text
389
+ .split(/[\n.;:]+/g)
390
+ .map((fragment) => fragment.trim())
391
+ .filter((fragment) => fragment.length >= 10)
392
+ .slice(0, maxItems * 3);
393
+ for (const fragment of fragments) {
394
+ pushUnique(fragment);
395
+ if (seen.size >= maxItems)
396
+ break;
397
+ }
398
+ }
399
+ return Array.from(seen).slice(0, maxItems);
400
+ }
401
+ function deriveAutoDelegateProfile(baseProfile, description, prompt) {
402
+ const signal = `${description}\n${prompt}`.toLowerCase();
403
+ const writeIntent = /\b(?:implement|fix|patch|refactor|rewrite|edit|update|migrate|change|write|apply)\b/.test(signal);
404
+ if (baseProfile === "full")
405
+ return writeIntent ? "full" : "explore";
406
+ if (baseProfile === "meta")
407
+ return writeIntent ? "full" : "explore";
408
+ if (baseProfile === "iosm")
409
+ return writeIntent ? "full" : "explore";
410
+ if (baseProfile === "iosm_verifier")
411
+ return "iosm_verifier";
412
+ if (baseProfile === "cycle_planner")
413
+ return "cycle_planner";
414
+ if (baseProfile === "plan" || baseProfile === "iosm_analyst")
415
+ return "explore";
416
+ return "explore";
417
+ }
418
+ function pickAutoDelegateAgent(workstream, availableCustomNames) {
419
+ if (availableCustomNames.length === 0)
420
+ return undefined;
421
+ const normalizedWorkstream = workstream.toLowerCase();
422
+ const names = availableCustomNames.map((name) => ({ raw: name, normalized: name.toLowerCase() }));
423
+ const findByHint = (hints) => {
424
+ for (const hint of hints) {
425
+ const exact = names.find((item) => item.normalized === hint);
426
+ if (exact)
427
+ return exact.raw;
428
+ const contains = names.find((item) => item.normalized.includes(hint));
429
+ if (contains)
430
+ return contains.raw;
431
+ }
432
+ return undefined;
433
+ };
434
+ if (/\b(?:test|qa|coverage|verification|regression)\b/.test(normalizedWorkstream)) {
435
+ return findByHint(["qa_test_engineer", "qa", "tester", "verification"]);
436
+ }
437
+ if (/\b(?:ui|ux|design|layout|accessibility)\b/.test(normalizedWorkstream)) {
438
+ return findByHint(["uiux_top_senior", "uiux", "ui", "ux", "design"]);
439
+ }
440
+ if (/\b(?:architecture|codebase|refactor|security|rbac|auth|database|api)\b/.test(normalizedWorkstream)) {
441
+ return findByHint(["codebase_auditor", "architect", "security", "backend"]);
442
+ }
443
+ return undefined;
444
+ }
445
+ function buildAutoDelegationPrompt(input) {
446
+ const objective = truncateForDelegationContext(`${input.rootDescription}\n\n${input.rootPrompt}`);
447
+ return normalizeSpacing([
448
+ `Workstream ${input.ordinal}/${input.total}: ${input.streamTitle}`,
449
+ "Scope:",
450
+ `- Own this stream end-to-end and avoid duplicating sibling streams.`,
451
+ `- Produce concrete findings/changes for this stream only.`,
452
+ "Coordinator objective:",
453
+ objective,
454
+ ].join("\n"));
455
+ }
456
+ function synthesizeDelegationRequests(input) {
457
+ const desiredTotal = Math.max(0, Math.min(input.maxDelegations, input.minDelegationsPreferred));
458
+ const missing = Math.max(0, desiredTotal - input.currentDelegates);
459
+ if (missing <= 0)
460
+ return [];
461
+ const combined = `${input.description}\n${input.prompt}`.trim();
462
+ const extracted = extractDelegationWorkstreams(combined, Math.max(missing, desiredTotal));
463
+ const fallbackByIndex = [
464
+ "Architecture and structure analysis",
465
+ "Behavioral verification and tests",
466
+ "Risk, regressions, and remediation",
467
+ "Integration and dependency checks",
468
+ "Delivery summary and rollout constraints",
469
+ ];
470
+ const titles = [];
471
+ for (const stream of extracted) {
472
+ titles.push(stream);
473
+ if (titles.length >= missing)
474
+ break;
475
+ }
476
+ for (let index = 0; titles.length < missing && index < fallbackByIndex.length; index += 1) {
477
+ titles.push(fallbackByIndex[index]);
478
+ }
479
+ while (titles.length < missing) {
480
+ titles.push(`Independent workstream ${titles.length + 1}`);
481
+ }
482
+ const defaultProfile = deriveAutoDelegateProfile(input.baseProfile, input.description, input.prompt);
483
+ const synthesizedLockKey = writeCapableProfiles.has(defaultProfile) ? "auto-synth-write-lock" : undefined;
484
+ return titles.map((streamTitle, index) => ({
485
+ description: `Auto: ${streamTitle}`,
486
+ profile: defaultProfile,
487
+ agent: pickAutoDelegateAgent(streamTitle, input.availableCustomNames),
488
+ prompt: buildAutoDelegationPrompt({
489
+ streamTitle,
490
+ rootDescription: input.description,
491
+ rootPrompt: input.prompt,
492
+ ordinal: input.currentDelegates + index + 1,
493
+ total: input.currentDelegates + titles.length,
494
+ }),
495
+ cwd: undefined,
496
+ lockKey: synthesizedLockKey,
497
+ model: undefined,
498
+ isolation: undefined,
499
+ dependsOn: undefined,
500
+ }));
501
+ }
334
502
  function withDelegationPrompt(basePrompt, depthRemaining, maxDelegations, minDelegationsPreferred = 0) {
335
503
  const protocol = buildDelegationProtocolPrompt(depthRemaining, maxDelegations, minDelegationsPreferred);
336
504
  return `${basePrompt}\n\n${protocol}`;
@@ -348,7 +516,10 @@ function buildSharedMemoryGuidance(runId, taskId) {
348
516
  "Guidelines:",
349
517
  "- Use scope=run for cross-agent data and scope=task for task-local notes.",
350
518
  "- Keep entries compact and key-based (for example: findings/auth, plan/step-1, risks/session).",
519
+ "- Prefer one canonical key per stream and deduplicate updates; avoid redundant writes in loops.",
351
520
  "- Read before overwrite when collaborating on the same key.",
521
+ "- Use if_version CAS for contested updates on shared keys.",
522
+ "- Use mode=append only for log/timeline keys; use mode=set for canonical state.",
352
523
  "[/SHARED_MEMORY]",
353
524
  ].join("\n");
354
525
  }
@@ -366,19 +537,21 @@ function parseDelegationRequests(output, maxRequests) {
366
537
  return "";
367
538
  }
368
539
  const attrs = {};
369
- for (const match of attrsRaw.matchAll(/([A-Za-z_][A-Za-z0-9_-]*)="([^"]*)"/g)) {
370
- attrs[match[1].toLowerCase()] = match[2];
540
+ for (const match of attrsRaw.matchAll(/([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
541
+ attrs[match[1].toLowerCase()] = (match[2] ?? match[3] ?? "").trim();
371
542
  }
372
543
  const prompt = normalizeSpacing(bodyRaw ?? "");
373
544
  if (!prompt) {
374
545
  warnings.push(`Ignored delegation block with empty prompt.`);
375
546
  return "";
376
547
  }
377
- const profile = (attrs.profile ?? "explore").trim();
378
- if (!(profile in toolsByProfile)) {
379
- warnings.push(`Ignored delegation block with unknown profile "${profile}".`);
548
+ const profileRaw = (attrs.profile ?? "explore").trim();
549
+ if (!profileRaw) {
550
+ warnings.push(`Ignored delegation block with empty profile.`);
380
551
  return "";
381
552
  }
553
+ const normalizedProfile = profileRaw.toLowerCase();
554
+ const profile = isValidProfileName(normalizedProfile) ? normalizedProfile : profileRaw;
382
555
  const isolationRaw = (attrs.isolation ?? "").trim().toLowerCase();
383
556
  const isolation = isolationRaw === "worktree" ? "worktree" : isolationRaw === "none" ? "none" : undefined;
384
557
  if (isolationRaw && !isolation) {
@@ -409,6 +582,9 @@ function parseDelegationRequests(output, maxRequests) {
409
582
  cleanedOutput: normalizeSpacing(cleaned),
410
583
  };
411
584
  }
585
+ function isBackgroundSafeToolset(tools) {
586
+ return tools.every((toolName) => !backgroundUnsafeTools.has(toolName));
587
+ }
412
588
  function getCwdLockKey(cwd) {
413
589
  // Normalize lock key to keep behavior consistent across path aliases.
414
590
  return path.resolve(cwd).toLowerCase();
@@ -694,12 +870,26 @@ export function createTaskTool(cwd, runner, options) {
694
870
  const resolveCustom = (name) => {
695
871
  if (!name || !options?.resolveCustomSubagent)
696
872
  return undefined;
697
- return options.resolveCustomSubagent(name);
873
+ const trimmed = name.trim();
874
+ if (!trimmed)
875
+ return undefined;
876
+ const resolved = options.resolveCustomSubagent(trimmed);
877
+ if (resolved)
878
+ return resolved;
879
+ const lowered = trimmed.toLowerCase();
880
+ if (lowered !== trimmed) {
881
+ return options.resolveCustomSubagent(lowered);
882
+ }
883
+ return undefined;
698
884
  };
699
885
  let normalizedAgentName = agentName?.trim() || undefined;
700
886
  let customSubagent = resolveCustom(normalizedAgentName);
701
- let normalizedProfile = profile?.trim().toLowerCase() || customSubagent?.profile?.trim().toLowerCase() || "full";
887
+ const requestedProfileRaw = profile?.trim() || undefined;
702
888
  const normalizedHostProfile = options?.getHostProfileName?.()?.trim().toLowerCase() ?? options?.hostProfileName?.trim().toLowerCase();
889
+ const hostProfileFallback = normalizedHostProfile && isValidProfileName(normalizedHostProfile) ? normalizedHostProfile : "full";
890
+ let normalizedProfile = requestedProfileRaw?.toLowerCase() ||
891
+ customSubagent?.profile?.trim().toLowerCase() ||
892
+ hostProfileFallback;
703
893
  if (normalizedAgentName && !customSubagent) {
704
894
  const available = availableCustomNames.length > 0 ? ` Available custom agents: ${availableCustomNames.join(", ")}.` : "";
705
895
  throw new Error(`Unknown subagent: ${normalizedAgentName}.${available}`);
@@ -713,17 +903,24 @@ export function createTaskTool(cwd, runner, options) {
713
903
  normalizedProfile = (profileAsAgent.profile ?? "full").trim().toLowerCase();
714
904
  }
715
905
  }
716
- if (!toolsByProfile[normalizedProfile]) {
717
- normalizedProfile = "full";
906
+ if (!customSubagent && !isValidProfileName(normalizedProfile)) {
907
+ throw new Error(`Unknown profile "${requestedProfileRaw ?? normalizedProfile}". Valid profiles: ${Object.keys(toolsByProfile).join(", ")}.`);
908
+ }
909
+ const effectiveProfileCandidate = (customSubagent?.profile ?? normalizedProfile).trim().toLowerCase();
910
+ if (!isValidProfileName(effectiveProfileCandidate)) {
911
+ throw new Error(`Invalid resolved profile "${effectiveProfileCandidate}". Valid profiles: ${Object.keys(toolsByProfile).join(", ")}.`);
718
912
  }
719
- const effectiveProfile = customSubagent?.profile ?? normalizedProfile;
913
+ const effectiveProfile = effectiveProfileCandidate;
720
914
  let tools = customSubagent?.tools
721
915
  ? [...customSubagent.tools]
722
- : [...(toolsByProfile[effectiveProfile] ?? toolsByProfile.explore)];
916
+ : [...toolsByProfile[effectiveProfile]];
723
917
  if (customSubagent?.disallowedTools?.length) {
724
918
  const blocked = new Set(customSubagent.disallowedTools);
725
919
  tools = tools.filter((tool) => !blocked.has(tool));
726
920
  }
921
+ if (isReadOnlyProfileName(normalizedHostProfile) && tools.some((tool) => writeCapableTools.has(tool))) {
922
+ throw new Error(`Host profile "${normalizedHostProfile}" is read-only. Switch to full/meta/iosm to launch write-capable subtasks.`);
923
+ }
727
924
  const delegationDepth = maxDelegationDepthFromEnv;
728
925
  const requestedDelegateParallelHint = typeof delegateParallelHint === "number" && Number.isInteger(delegateParallelHint)
729
926
  ? Math.max(1, Math.min(MAX_SUBAGENT_DELEGATE_PARALLEL, delegateParallelHint))
@@ -735,13 +932,22 @@ export function createTaskTool(cwd, runner, options) {
735
932
  const effectiveDelegationDepth = effectiveProfile === "meta" || normalizedHostProfile === "meta" || normalizedAgentName?.toLowerCase().includes("orchestrator")
736
933
  ? Math.max(delegationDepth, 2)
737
934
  : delegationDepth;
935
+ const orchestratedRunContext = !!(orchestrationRunId && orchestrationTaskId);
936
+ const strictDelegationContract = effectiveProfile === "meta" ||
937
+ normalizedHostProfile === "meta" ||
938
+ normalizedAgentName?.toLowerCase().includes("orchestrator") ||
939
+ (orchestratedRunContext && (effectiveDelegateParallelHint ?? 0) >= 2);
738
940
  let effectiveMaxDelegations = Math.max(0, Math.min(maxDelegationsPerTaskFromEnv, effectiveDelegateParallelHint ?? maxDelegationsPerTaskFromEnv));
739
941
  let effectiveMaxDelegateParallel = Math.max(1, Math.min(maxDelegatedParallelFromEnv, effectiveDelegateParallelHint ?? maxDelegatedParallelFromEnv));
740
- const preferredDelegationFloor = effectiveProfile === "meta" || normalizedHostProfile === "meta" ? 3 : 2;
942
+ const isMetaDelegationContext = effectiveProfile === "meta" || normalizedHostProfile === "meta";
943
+ const preferredDelegationFloorBase = isMetaDelegationContext ? 3 : 2;
944
+ const preferredDelegationFloorMin = isMetaDelegationContext ? 2 : 1;
945
+ const metaDelegationCapacityFloor = 3;
946
+ const preferredDelegationFloor = Math.max(preferredDelegationFloorMin, Math.min(preferredDelegationFloorBase, effectiveDelegateParallelHint ?? preferredDelegationFloorBase));
741
947
  const applyMetaDelegationFloor = requestedDelegateParallelHint === undefined &&
742
948
  (effectiveProfile === "meta" || normalizedHostProfile === "meta");
743
949
  if (applyMetaDelegationFloor) {
744
- effectiveMaxDelegations = Math.max(effectiveMaxDelegations, Math.min(maxDelegationsPerTaskFromEnv, preferredDelegationFloor));
950
+ effectiveMaxDelegations = Math.max(effectiveMaxDelegations, Math.min(maxDelegationsPerTaskFromEnv, metaDelegationCapacityFloor));
745
951
  effectiveMaxDelegateParallel = Math.max(effectiveMaxDelegateParallel, Math.min(maxDelegatedParallelFromEnv, preferredDelegationFloor));
746
952
  }
747
953
  const minDelegationsPreferred = (effectiveDelegateParallelHint ?? 0) >= 2 && effectiveMaxDelegations >= 2
@@ -764,8 +970,10 @@ export function createTaskTool(cwd, runner, options) {
764
970
  if (!existsSync(requestedSubagentCwd) || !statSync(requestedSubagentCwd).isDirectory()) {
765
971
  throw new Error(`Subagent cwd does not exist or is not a directory: ${requestedSubagentCwd}`);
766
972
  }
767
- if (runInBackground && writeCapableProfiles.has(effectiveProfile)) {
768
- throw new Error(`Background policy violation: profile "${effectiveProfile}" is write-capable. Use explore/plan/iosm_analyst for background mode.`);
973
+ if (runInBackground && !isBackgroundSafeToolset(tools)) {
974
+ throw new Error(`Background policy violation: profile "${effectiveProfile}" has mutable tools (${tools
975
+ .filter((toolName) => backgroundUnsafeTools.has(toolName))
976
+ .join(", ")}). Background mode requires read-only toolsets. Safe baseline profiles: ${backgroundSafeProfiles.join(", ")}.`);
769
977
  }
770
978
  const useWorktree = isolation === "worktree";
771
979
  const queuedAt = Date.now();
@@ -821,6 +1029,42 @@ export function createTaskTool(cwd, runner, options) {
821
1029
  let subagentCwd = requestedSubagentCwd;
822
1030
  let worktreePath;
823
1031
  let runStats;
1032
+ const heldWriteLocks = new Map();
1033
+ const acquireLocalWriteLock = async (rawLockKey) => {
1034
+ const trimmed = rawLockKey?.trim();
1035
+ if (!trimmed)
1036
+ return undefined;
1037
+ const normalizedKey = getCwdLockKey(trimmed);
1038
+ const existing = heldWriteLocks.get(normalizedKey);
1039
+ if (existing) {
1040
+ existing.count += 1;
1041
+ return () => {
1042
+ const current = heldWriteLocks.get(normalizedKey);
1043
+ if (!current)
1044
+ return;
1045
+ current.count -= 1;
1046
+ if (current.count <= 0) {
1047
+ heldWriteLocks.delete(normalizedKey);
1048
+ current.release();
1049
+ cleanupWriteLock(trimmed);
1050
+ }
1051
+ };
1052
+ }
1053
+ const lock = getOrCreateWriteLock(trimmed);
1054
+ const release = await lock.acquire();
1055
+ heldWriteLocks.set(normalizedKey, { count: 1, release });
1056
+ return () => {
1057
+ const current = heldWriteLocks.get(normalizedKey);
1058
+ if (!current)
1059
+ return;
1060
+ current.count -= 1;
1061
+ if (current.count <= 0) {
1062
+ heldWriteLocks.delete(normalizedKey);
1063
+ current.release();
1064
+ cleanupWriteLock(trimmed);
1065
+ }
1066
+ };
1067
+ };
824
1068
  try {
825
1069
  throwIfAborted();
826
1070
  if (orchestrationRunId && orchestrationTaskId) {
@@ -882,8 +1126,7 @@ export function createTaskTool(cwd, runner, options) {
882
1126
  // Parallel orchestration should remain truly parallel by default.
883
1127
  // Serialize write-capable agents only when an explicit lock_key is provided.
884
1128
  if (explicitRootLockKey) {
885
- const lock = getOrCreateWriteLock(explicitRootLockKey);
886
- releaseWriteLock = await lock.acquire();
1129
+ releaseWriteLock = await acquireLocalWriteLock(explicitRootLockKey);
887
1130
  }
888
1131
  }
889
1132
  if (useWorktree) {
@@ -1052,8 +1295,33 @@ export function createTaskTool(cwd, runner, options) {
1052
1295
  runStats = secondPass.stats ?? runStats;
1053
1296
  parsedDelegation = parseDelegationRequests(output, effectiveDelegationDepth > 0 ? effectiveMaxDelegations : 0);
1054
1297
  }
1298
+ if (minDelegationsPreferred > 0 &&
1299
+ parsedDelegation.requests.length === 0 &&
1300
+ strictDelegationContract) {
1301
+ const impossibleMatch = output.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
1302
+ if (!impossibleMatch) {
1303
+ const synthesizedRequests = synthesizeDelegationRequests({
1304
+ description,
1305
+ prompt,
1306
+ baseProfile: effectiveProfile,
1307
+ currentDelegates: parsedDelegation.requests.length,
1308
+ minDelegationsPreferred,
1309
+ maxDelegations: effectiveMaxDelegations,
1310
+ availableCustomNames,
1311
+ });
1312
+ if (synthesizedRequests.length > 0) {
1313
+ parsedDelegation.requests.push(...synthesizedRequests);
1314
+ delegationWarnings.push(`Delegation auto-fanout: synthesized ${synthesizedRequests.length} delegate(s) to satisfy parallelism contract.`);
1315
+ }
1316
+ }
1317
+ }
1055
1318
  if (minDelegationsPreferred > 0 && parsedDelegation.requests.length < minDelegationsPreferred) {
1056
- const impossibleReason = output.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im)?.[1]?.trim() ?? "not provided";
1319
+ const impossibleMatch = output.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
1320
+ const impossibleReason = impossibleMatch?.[1]?.trim() ?? "not provided";
1321
+ if (strictDelegationContract && parsedDelegation.requests.length === 0 && !impossibleMatch) {
1322
+ throw new Error(`Delegation contract violated: expected >=${minDelegationsPreferred} delegates, got ${parsedDelegation.requests.length}. ` +
1323
+ `Provide nested <delegate_task> fan-out or an explicit "DELEGATION_IMPOSSIBLE: <reason>" line.`);
1324
+ }
1057
1325
  delegationWarnings.push(`Delegation fallback: kept single-agent execution (preferred >=${minDelegationsPreferred} delegates, got ${parsedDelegation.requests.length}). Reason: ${impossibleReason}.`);
1058
1326
  }
1059
1327
  output = parsedDelegation.cleanedOutput;
@@ -1127,25 +1395,60 @@ export function createTaskTool(cwd, runner, options) {
1127
1395
  return { sections: [], warnings: [] };
1128
1396
  }
1129
1397
  const nestedWarnings = [];
1130
- const sections = [];
1131
- for (const [nestedIndex, nestedRequest] of requests.entries()) {
1398
+ const sectionsByIndex = new Array(requests.length);
1399
+ const statuses = Array.from({ length: requests.length }, () => "pending");
1400
+ const totalNested = requests.length;
1401
+ const normalizedDependsOn = requests.map((request, index) => {
1402
+ const current = index + 1;
1403
+ const raw = request.dependsOn ?? [];
1404
+ const unique = new Set();
1405
+ for (const dep of raw) {
1406
+ if (!Number.isInteger(dep) || dep <= 0 || dep > totalNested || dep === current) {
1407
+ nestedWarnings.push(`Nested delegated task ${lineage}${current} has invalid depends_on reference "${dep}" and it was ignored.`);
1408
+ continue;
1409
+ }
1410
+ unique.add(dep);
1411
+ }
1412
+ return Array.from(unique).sort((a, b) => a - b);
1413
+ });
1414
+ const statusOf = (index) => statuses[index] ?? "pending";
1415
+ const markNestedFailed = (nestedIndex, requestLabel, nestedRequest, nestedProfileLabel, message, cause) => {
1416
+ statuses[nestedIndex] = "failed";
1417
+ recordFailureCause(cause);
1418
+ delegatedTasks += 1;
1419
+ delegatedFailed += 1;
1420
+ sectionsByIndex[nestedIndex] =
1421
+ `###### ${requestLabel}. ${nestedRequest.description} (${nestedProfileLabel})\nERROR [cause=${cause}]: ${message}`;
1422
+ };
1423
+ const runNestedDelegate = async (nestedIndex) => {
1424
+ const nestedRequest = requests[nestedIndex];
1132
1425
  const requestLabel = `${lineage}${nestedIndex + 1}`;
1133
- const requestedNestedAgent = nestedRequest.agent?.trim() || undefined;
1134
- const nestedCustomSubagent = resolveCustom(requestedNestedAgent);
1426
+ statuses[nestedIndex] = "running";
1427
+ let requestedNestedAgent = nestedRequest.agent?.trim() || undefined;
1428
+ let nestedCustomSubagent = resolveCustom(requestedNestedAgent);
1429
+ if (!nestedCustomSubagent && !requestedNestedAgent) {
1430
+ const profileAsAgent = resolveCustom(nestedRequest.profile);
1431
+ if (profileAsAgent) {
1432
+ nestedCustomSubagent = profileAsAgent;
1433
+ requestedNestedAgent = profileAsAgent.name;
1434
+ }
1435
+ }
1135
1436
  if (requestedNestedAgent && !nestedCustomSubagent) {
1136
1437
  nestedWarnings.push(`Nested delegated task "${nestedRequest.description}" requested unknown agent "${requestedNestedAgent}". Falling back to profile "${nestedRequest.profile}".`);
1137
1438
  }
1138
- const nestedProfileRaw = nestedCustomSubagent?.profile ?? nestedRequest.profile;
1139
- const normalizedNestedProfile = nestedProfileRaw.trim().toLowerCase();
1140
- const nestedProfile = normalizedNestedProfile && toolsByProfile[normalizedNestedProfile]
1141
- ? normalizedNestedProfile
1142
- : "full";
1439
+ const nestedProfileCandidate = (nestedCustomSubagent?.profile ?? nestedRequest.profile).trim();
1440
+ const normalizedNestedProfile = nestedProfileCandidate.toLowerCase();
1441
+ if (!isValidProfileName(normalizedNestedProfile)) {
1442
+ markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedProfileCandidate || "unknown", `nested delegate skipped: unknown profile "${nestedProfileCandidate || nestedRequest.profile}"`, "logic_error");
1443
+ return;
1444
+ }
1445
+ const nestedProfile = normalizedNestedProfile;
1143
1446
  const nestedProfileLabel = nestedCustomSubagent?.name
1144
1447
  ? `${nestedCustomSubagent.name}/${nestedProfile}`
1145
1448
  : nestedProfile;
1146
1449
  let nestedTools = nestedCustomSubagent?.tools
1147
1450
  ? [...nestedCustomSubagent.tools]
1148
- : [...(toolsByProfile[nestedProfile] ?? toolsByProfile.explore)];
1451
+ : [...toolsByProfile[nestedProfile]];
1149
1452
  if (nestedCustomSubagent?.disallowedTools?.length) {
1150
1453
  const blocked = new Set(nestedCustomSubagent.disallowedTools);
1151
1454
  nestedTools = nestedTools.filter((tool) => !blocked.has(tool));
@@ -1158,19 +1461,15 @@ export function createTaskTool(cwd, runner, options) {
1158
1461
  ? path.resolve(parentCwd, nestedRequest.cwd)
1159
1462
  : nestedCustomSubagent?.cwd ?? parentCwd;
1160
1463
  if (!existsSync(requestedNestedCwd) || !statSync(requestedNestedCwd).isDirectory()) {
1161
- recordFailureCause("dependency_env");
1162
- delegatedFailed += 1;
1163
- delegatedTasks += 1;
1164
- sections.push(`###### ${requestLabel}. ${nestedRequest.description} (${nestedProfileLabel})\nERROR [cause=dependency_env]: nested delegate skipped: missing cwd`);
1165
- continue;
1464
+ markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedProfileLabel, "nested delegate skipped: missing cwd", "dependency_env");
1465
+ return;
1166
1466
  }
1167
1467
  let nestedReleaseLock;
1168
1468
  let nestedReleaseIsolation;
1169
1469
  let nestedCwd = requestedNestedCwd;
1170
1470
  try {
1171
1471
  if (writeCapableProfiles.has(nestedProfile) && nestedRequest.lockKey?.trim()) {
1172
- const lock = getOrCreateWriteLock(nestedRequest.lockKey.trim());
1173
- nestedReleaseLock = await lock.acquire();
1472
+ nestedReleaseLock = await acquireLocalWriteLock(nestedRequest.lockKey.trim());
1174
1473
  }
1175
1474
  if (nestedRequest.isolation === "worktree") {
1176
1475
  const isolated = provisionWorktree(cwd, requestedNestedCwd, `${runId}_nested_${requestLabel.replace(/\./g, "_")}`);
@@ -1212,6 +1511,7 @@ export function createTaskTool(cwd, runner, options) {
1212
1511
  const nestedStats = typeof nestedResult === "string" ? undefined : nestedResult.stats;
1213
1512
  delegatedTasks += 1;
1214
1513
  delegatedSucceeded += 1;
1514
+ statuses[nestedIndex] = "done";
1215
1515
  delegatedStats.toolCallsStarted += nestedStats?.toolCallsStarted ?? 0;
1216
1516
  delegatedStats.toolCallsCompleted += nestedStats?.toolCallsCompleted ?? 0;
1217
1517
  delegatedStats.assistantMessages += nestedStats?.assistantMessages ?? 0;
@@ -1226,37 +1526,105 @@ export function createTaskTool(cwd, runner, options) {
1226
1526
  nestedSection = `${nestedSection}\n\n##### Nested Delegated Subtasks\n\n${deeper.sections.join("\n\n")}`;
1227
1527
  }
1228
1528
  }
1229
- sections.push(nestedSection);
1529
+ sectionsByIndex[nestedIndex] = nestedSection;
1230
1530
  }
1231
1531
  catch (error) {
1232
1532
  const message = error instanceof Error ? error.message : String(error);
1233
1533
  const cause = classifyFailureCause(message);
1234
- recordFailureCause(cause);
1235
- delegatedTasks += 1;
1236
- delegatedFailed += 1;
1237
- sections.push(`###### ${requestLabel}. ${nestedRequest.description} (${nestedProfileLabel})\nERROR [cause=${cause}]: ${message}`);
1534
+ markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedProfileLabel, message, cause);
1238
1535
  }
1239
1536
  finally {
1240
1537
  nestedReleaseIsolation?.();
1241
1538
  nestedReleaseLock?.();
1242
- cleanupWriteLock(nestedRequest.lockKey?.trim());
1243
1539
  }
1540
+ };
1541
+ const pendingIndices = new Set(Array.from({ length: totalNested }, (_v, i) => i));
1542
+ const runningNested = new Map();
1543
+ const maxNestedParallel = Math.max(1, Math.min(totalNested, effectiveMaxDelegateParallel));
1544
+ const resolveBlockedByFailedDependencies = () => {
1545
+ let changed = false;
1546
+ for (const nestedIndex of Array.from(pendingIndices)) {
1547
+ const deps = normalizedDependsOn[nestedIndex] ?? [];
1548
+ if (deps.length === 0)
1549
+ continue;
1550
+ const failedDep = deps.find((dep) => statusOf(dep - 1) === "failed");
1551
+ if (!failedDep)
1552
+ continue;
1553
+ pendingIndices.delete(nestedIndex);
1554
+ const nestedRequest = requests[nestedIndex];
1555
+ const requestLabel = `${lineage}${nestedIndex + 1}`;
1556
+ markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedRequest.profile, `nested delegate skipped: dependency ${lineage}${failedDep} failed`, "logic_error");
1557
+ changed = true;
1558
+ }
1559
+ return changed;
1560
+ };
1561
+ const launchReadyNested = () => {
1562
+ let launched = false;
1563
+ while (runningNested.size < maxNestedParallel) {
1564
+ let nextIndex;
1565
+ for (const nestedIndex of pendingIndices) {
1566
+ const deps = normalizedDependsOn[nestedIndex] ?? [];
1567
+ const allDone = deps.every((dep) => statusOf(dep - 1) === "done");
1568
+ if (allDone) {
1569
+ nextIndex = nestedIndex;
1570
+ break;
1571
+ }
1572
+ }
1573
+ if (nextIndex === undefined)
1574
+ break;
1575
+ pendingIndices.delete(nextIndex);
1576
+ const promise = runNestedDelegate(nextIndex).finally(() => {
1577
+ runningNested.delete(nextIndex);
1578
+ });
1579
+ runningNested.set(nextIndex, promise);
1580
+ launched = true;
1581
+ }
1582
+ return launched;
1583
+ };
1584
+ while (pendingIndices.size > 0 || runningNested.size > 0) {
1585
+ throwIfAborted();
1586
+ const changed = resolveBlockedByFailedDependencies();
1587
+ const launched = launchReadyNested();
1588
+ if (runningNested.size === 0) {
1589
+ if (pendingIndices.size > 0 && !changed && !launched) {
1590
+ for (const nestedIndex of Array.from(pendingIndices)) {
1591
+ pendingIndices.delete(nestedIndex);
1592
+ const nestedRequest = requests[nestedIndex];
1593
+ const deps = normalizedDependsOn[nestedIndex] ?? [];
1594
+ const requestLabel = `${lineage}${nestedIndex + 1}`;
1595
+ markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedRequest.profile, `nested delegate blocked: unresolved depends_on (${deps.join(", ") || "unknown"})`, "logic_error");
1596
+ }
1597
+ }
1598
+ break;
1599
+ }
1600
+ await Promise.race(Array.from(runningNested.values()));
1244
1601
  }
1602
+ const sections = sectionsByIndex.filter((section) => typeof section === "string" && section.trim().length > 0);
1245
1603
  return { sections, warnings: nestedWarnings };
1246
1604
  };
1247
1605
  const runDelegate = async (index) => {
1248
1606
  throwIfAborted();
1249
1607
  const request = parsedDelegation.requests[index];
1250
- const requestedChildAgent = request.agent?.trim() || undefined;
1251
- const childCustomSubagent = resolveCustom(requestedChildAgent);
1608
+ let requestedChildAgent = request.agent?.trim() || undefined;
1609
+ let childCustomSubagent = resolveCustom(requestedChildAgent);
1610
+ if (!childCustomSubagent && !requestedChildAgent) {
1611
+ const profileAsAgent = resolveCustom(request.profile);
1612
+ if (profileAsAgent) {
1613
+ childCustomSubagent = profileAsAgent;
1614
+ requestedChildAgent = profileAsAgent.name;
1615
+ }
1616
+ }
1252
1617
  if (requestedChildAgent && !childCustomSubagent) {
1253
1618
  delegationWarnings.push(`Delegated task "${request.description}" requested unknown agent "${requestedChildAgent}". Falling back to profile "${request.profile}".`);
1254
1619
  }
1255
- const childProfileRaw = childCustomSubagent?.profile ?? request.profile;
1256
- const normalizedChildProfile = childProfileRaw.trim().toLowerCase();
1257
- const childProfile = normalizedChildProfile && toolsByProfile[normalizedChildProfile]
1258
- ? normalizedChildProfile
1259
- : "full";
1620
+ const childProfileRaw = (childCustomSubagent?.profile ?? request.profile).trim();
1621
+ const normalizedChildProfile = childProfileRaw.toLowerCase();
1622
+ if (!isValidProfileName(normalizedChildProfile)) {
1623
+ recordFailureCause("logic_error");
1624
+ markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} skipped: unknown profile "${childProfileRaw || request.profile}"`, `Delegated task "${request.description}" requested unknown profile "${childProfileRaw || request.profile}".`, "logic_error");
1625
+ return;
1626
+ }
1627
+ const childProfile = normalizedChildProfile;
1260
1628
  const childProfileLabel = childCustomSubagent?.name
1261
1629
  ? `${childCustomSubagent.name}/${childProfile}`
1262
1630
  : childProfile;
@@ -1277,7 +1645,7 @@ export function createTaskTool(cwd, runner, options) {
1277
1645
  });
1278
1646
  let childTools = childCustomSubagent?.tools
1279
1647
  ? [...childCustomSubagent.tools]
1280
- : [...(toolsByProfile[childProfile] ?? toolsByProfile.explore)];
1648
+ : [...toolsByProfile[childProfile]];
1281
1649
  if (childCustomSubagent?.disallowedTools?.length) {
1282
1650
  const blocked = new Set(childCustomSubagent.disallowedTools);
1283
1651
  childTools = childTools.filter((tool) => !blocked.has(tool));
@@ -1306,8 +1674,7 @@ export function createTaskTool(cwd, runner, options) {
1306
1674
  try {
1307
1675
  throwIfAborted();
1308
1676
  if (writeCapableProfiles.has(childProfile) && explicitChildLock) {
1309
- const lock = getOrCreateWriteLock(explicitChildLock);
1310
- childReleaseLock = await lock.acquire();
1677
+ childReleaseLock = await acquireLocalWriteLock(explicitChildLock);
1311
1678
  throwIfAborted();
1312
1679
  }
1313
1680
  if (request.isolation === "worktree") {
@@ -1477,10 +1844,36 @@ export function createTaskTool(cwd, runner, options) {
1477
1844
  childOutput = await runChildPass(enforcedChildPrompt);
1478
1845
  parsedChildDelegation = parseDelegationRequests(childOutput, effectiveDelegationDepth > 1 ? effectiveMaxDelegations : 0);
1479
1846
  }
1847
+ if (childMinDelegationsPreferred > 0 &&
1848
+ parsedChildDelegation.requests.length === 0 &&
1849
+ strictDelegationContract) {
1850
+ const impossibleMatch = childOutput.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
1851
+ if (!impossibleMatch) {
1852
+ const synthesizedChildRequests = synthesizeDelegationRequests({
1853
+ description: request.description,
1854
+ prompt: request.prompt,
1855
+ baseProfile: childProfile,
1856
+ currentDelegates: parsedChildDelegation.requests.length,
1857
+ minDelegationsPreferred: childMinDelegationsPreferred,
1858
+ maxDelegations: effectiveMaxDelegations,
1859
+ availableCustomNames,
1860
+ });
1861
+ if (synthesizedChildRequests.length > 0) {
1862
+ parsedChildDelegation.requests.push(...synthesizedChildRequests);
1863
+ delegationWarnings.push(`Child ${index + 1}: delegation auto-fanout synthesized ${synthesizedChildRequests.length} nested delegate(s).`);
1864
+ }
1865
+ }
1866
+ }
1480
1867
  if (childMinDelegationsPreferred > 0 &&
1481
1868
  parsedChildDelegation.requests.length < childMinDelegationsPreferred) {
1482
- const impossibleReason = childOutput.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im)?.[1]?.trim() ??
1483
- "not provided";
1869
+ const impossibleMatch = childOutput.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
1870
+ const impossibleReason = impossibleMatch?.[1]?.trim() ?? "not provided";
1871
+ if (strictDelegationContract &&
1872
+ parsedChildDelegation.requests.length === 0 &&
1873
+ !impossibleMatch) {
1874
+ throw new Error(`Delegation contract violated for child ${index + 1}: expected >=${childMinDelegationsPreferred} nested delegates, got ${parsedChildDelegation.requests.length}. ` +
1875
+ `Provide nested <delegate_task> fan-out or "DELEGATION_IMPOSSIBLE: <reason>".`);
1876
+ }
1484
1877
  delegationWarnings.push(`Child ${index + 1}: delegation fallback (preferred >=${childMinDelegationsPreferred}, got ${parsedChildDelegation.requests.length}). Reason: ${impossibleReason}.`);
1485
1878
  }
1486
1879
  childOutput = parsedChildDelegation.cleanedOutput;
@@ -1535,7 +1928,6 @@ export function createTaskTool(cwd, runner, options) {
1535
1928
  finally {
1536
1929
  childReleaseIsolation?.();
1537
1930
  childReleaseLock?.();
1538
- cleanupWriteLock(explicitChildLock);
1539
1931
  }
1540
1932
  };
1541
1933
  const resolveBlockedByFailedDependencies = () => {
@@ -1760,7 +2152,6 @@ export function createTaskTool(cwd, runner, options) {
1760
2152
  finally {
1761
2153
  releaseIsolation?.();
1762
2154
  releaseWriteLock?.();
1763
- cleanupWriteLock(explicitRootLockKey);
1764
2155
  releaseSlot?.();
1765
2156
  releaseRunSlot?.();
1766
2157
  if (orchestrationRunId) {