pi-subagents 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { discoverAgents, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
7
- import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
7
+ import { getArtifactsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
8
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
9
9
  import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilient-json.ts";
10
10
  import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
@@ -13,7 +13,6 @@ import {
13
13
  beginForegroundChild,
14
14
  finishForegroundChild,
15
15
  foregroundSchedulingSettled,
16
- retainForegroundSchedulingOwner,
17
16
  settleForegroundSchedulingOwner,
18
17
  updateForegroundChild,
19
18
  } from "./foreground-control.ts";
@@ -28,36 +27,27 @@ import { normalizePublicSubagentExecution } from "../../extension/public-executi
28
27
  import { runSync } from "./execution.ts";
29
28
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
30
29
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
31
- import { buildModelCandidates, inheritsParentModel, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
30
+ import { buildModelCandidates, inheritsParentModel, normalizeParentModel, resolveEffectiveSubagentModel, type ParentModel } from "../shared/model-fallback.ts";
32
31
  import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
33
32
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
34
- import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
35
33
  import { recordRun } from "../shared/run-history.ts";
36
34
  import {
37
- buildChainInstructions,
38
- writeInitialProgressFile,
39
35
  getStepAgents,
40
36
  isParallelStep,
41
37
  isDynamicParallelStep,
42
- resolveChainPath,
43
38
  resolveExistingReadPaths,
44
- resolveStepBehavior,
45
- suppressProgressForReadOnlyTask,
46
- taskDisallowsFileUpdates,
47
39
  type ChainStep,
48
40
  type DynamicParallelStep,
49
41
  type ParallelStep,
50
42
  type ParallelTaskItem,
51
- type ResolvedStepBehavior,
52
43
  type SequentialStep,
53
- type StepOverrides,
54
44
  } from "../../shared/settings.ts";
55
- import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
45
+ import { normalizeSkillInput } from "../../agents/skills.ts";
56
46
  import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
57
47
  import { updateActiveRunIndex } from "../background/active-run-index.ts";
58
48
  import { steeringReceipt } from "../background/steering.ts";
59
49
  import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
60
- import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
50
+ import { isScheduledRunAction } from "../background/scheduled-runs.ts";
61
51
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
62
52
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
63
53
  import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
@@ -67,7 +57,6 @@ import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
67
57
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
68
58
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
69
59
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
70
- import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
71
60
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
72
61
  import { claimRunFanoutBatch, claimRunFanoutBatchWithCommit, createRunFanoutBudget, decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor, RunFanoutLimitError, RUN_FANOUT_BUDGET_ENV, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
73
62
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
@@ -77,10 +66,9 @@ import { isAgentContractV1 } from "../shared/agent-contract.ts";
77
66
  import { normalizeExtensionBindings, type ExtensionBindings } from "../shared/extension-bindings.ts";
78
67
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, outputPathMappingFromTask, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
79
68
  import { assertJsonSchemaObject, cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
80
- import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
69
+ import { compactForegroundDetails, getSingleResultOutput, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage, toAgentToolUsage } from "../../shared/utils.ts";
81
70
  import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
82
- import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
83
- import { discardPreservedWorktrees, formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
71
+ import { discardPreservedWorktrees, formatParallelHandoffError, formatParallelHandoffReference, formatStoredParallelHandoffCleanup, parallelHandoffPath, readParallelHandoffManifest, recordParallelHandoffMerge, recordParallelHandoffSupersession, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
84
72
  import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
85
73
  import {
86
74
  attachNestedChildrenToResultChildren,
@@ -125,19 +113,22 @@ import { resolveAuthorityDecision } from "../../policy/authority.ts";
125
113
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
126
114
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
127
115
  import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
116
+ import { executeWorkflowHostCommand, resolveWorkflowHostOutputClaimPath, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "../../workflows/host-command.ts";
128
117
  import { buildWorkflowReceipt, resolveWorkflowReceiptResumeEntry, writeWorkflowReceipt, type WorkflowReceipt, type WorkflowReceiptState } from "../../workflows/workflow-receipt.ts";
129
- import { workflowChildSummary } from "../../workflows/workflow-child-summary.ts";
118
+ import { upsertHostStep, validHostStepNodes } from "../shared/host-step-status.ts";
119
+ import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
120
+ import { parseWorkflowChildSummary, workflowChildSummary } from "../../workflows/workflow-child-summary.ts";
130
121
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
122
+ import { annotateWorkflowPreflightTrace, formatWorkflowPreflight, formatWorkflowPreflightWarnings, normalizeWorkflowPreflight, workflowPreflightWarnings } from "../../workflows/workflow-preflight.ts";
131
123
  import { claimWorkflowChildPermit, validateWorkflowChildPermitRoot, type WorkflowChildPermit, type WorkflowChildPermitContext } from "../../shared/workflow-child-permit.ts";
132
124
  import {
133
125
  cleanupWorktrees,
134
126
  createWorktrees,
135
127
  diffWorktrees,
136
- findWorktreeTaskCwdConflict,
137
128
  formatWorktreeDiffSummary,
138
- formatWorktreeTaskCwdConflict,
139
129
  type WorktreeSetup,
140
130
  } from "../shared/worktree.ts";
131
+ import { createWorktreeCleanupPlan, formatWorktreeCleanupPlan } from "../shared/worktree-cleanup-plan.ts";
141
132
  import {
142
133
  type AgentProgress,
143
134
  type AsyncJobState,
@@ -152,6 +143,7 @@ import {
152
143
  type ExtensionConfig,
153
144
  type ForegroundResumeChild,
154
145
  type ForegroundRunControl,
146
+ type HostStepNodeV1,
155
147
  type IntercomBridgeConfig,
156
148
  type IntercomEventBus,
157
149
  type JsonSchemaObject,
@@ -160,14 +152,13 @@ import {
160
152
  type NestedRunSummary,
161
153
  type OutputMode,
162
154
  type ResolvedControlConfig,
163
- type ResolvedTurnBudget,
164
155
  type ResolvedToolBudget,
165
156
  type RunFanoutBudgetDescriptor,
166
157
  type SteeringRecoveryDescriptor,
167
158
  type SingleResult,
168
159
  type SubagentChildStatusEvent,
169
160
  type ToolBudgetConfig,
170
- type TurnBudgetConfig,
161
+ type Usage,
171
162
  type UsageBudgetConfig,
172
163
  type WorkflowTerminalOutcome,
173
164
  type SubagentRunMode,
@@ -180,18 +171,21 @@ import {
180
171
  SUBAGENT_CONTROL_INTERCOM_EVENT,
181
172
  SUBAGENT_FOREGROUND_COMPLETE_EVENT,
182
173
  checkSubagentDepth,
183
- resolveTopLevelParallelConcurrency,
184
- resolveTopLevelParallelMaxTasks,
185
174
  resolveChildMaxSubagentDepth,
186
175
  resolveCurrentMaxSubagentDepth,
187
176
  resolveMaxSubagentSpawnsPerRun,
188
177
  wrapForkTask,
189
178
  type ScheduleOrigin,
190
179
  } from "../../shared/types.ts";
180
+ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
191
181
 
192
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
182
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "worktree.cleanup", "lane.recordMerge", "lane.recordSupersession", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
193
183
  const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "schedule.delete"]);
194
184
 
185
+ function resolveSteerDeliveryMode(mode: SubagentParamsLike["mode"]): SteerDeliveryMode | undefined {
186
+ return mode === "steer" || mode === "follow_up" || mode === "auto" ? mode : undefined;
187
+ }
188
+
195
189
  function editDistance(left: string, right: string): number {
196
190
  const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
197
191
  for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
@@ -287,6 +281,9 @@ export interface SubagentParamsLike {
287
281
  runId?: string;
288
282
  dir?: string;
289
283
  handoffPath?: string;
284
+ laneId?: string;
285
+ merge?: unknown;
286
+ supersession?: unknown;
290
287
  index?: number;
291
288
  childId?: string;
292
289
  view?: "fleet" | "transcript";
@@ -303,20 +300,27 @@ export interface SubagentParamsLike {
303
300
  resume?: string;
304
301
  message?: string;
305
302
  steeringRecovery?: boolean;
306
- mode?: SteerDeliveryMode;
303
+ mode?: SteerDeliveryMode | "plan" | "apply";
304
+ repo?: string;
305
+ planId?: string;
307
306
  workflowScript?: string;
308
307
  workflowScriptPath?: string;
308
+ preflight?: import("../../shared/types.ts").WorkflowPreflightV1;
309
309
  chatProgress?: "auto" | "off" | "live-card";
310
310
  isolation?: "none" | "worktree";
311
311
  step?: ChainStep;
312
312
  /** Internal workflow ownership metadata; not part of the public schema. */
313
313
  workflowParentRunId?: string;
314
314
  workflowKey?: string;
315
+ lane?: import("../../shared/types.ts").WorkflowLaneMetadata;
315
316
  /** Set by the scheduler so this run's completion can name the schedule that produced it. */
316
317
  scheduleOrigin?: ScheduleOrigin;
317
318
  workflowChildAsyncId?: string;
318
319
  workflowAwaitAsync?: boolean;
320
+ /** Internal async-workflow bridge: keep the live VM await pending across supervisor detachment. */
321
+ workflowAwaitDetached?: boolean;
319
322
  workflowParentDeadlineAt?: number;
323
+ workflowOutputClaimPath?: string;
320
324
  suppressRoutineResultIntercom?: boolean;
321
325
  /** Internal inherited cumulative run-tree budget. */
322
326
  runFanoutBudget?: RunFanoutBudgetDescriptor;
@@ -338,9 +342,6 @@ export interface SubagentParamsLike {
338
342
  maxRuntimeMs?: number;
339
343
  /** Optional hard per-tool-call timeout (ms). Known-fast tools also have a default. */
340
344
  toolTimeoutMs?: number;
341
- turnBudget?: TurnBudgetConfig;
342
- /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
343
- enforceHardTurnLimit?: boolean;
344
345
  toolBudget?: ToolBudgetConfig;
345
346
  usageBudget?: UsageBudgetConfig;
346
347
  clarify?: boolean;
@@ -400,12 +401,13 @@ interface ExecutorDeps {
400
401
  config: ExtensionConfig;
401
402
  asyncByDefault: boolean;
402
403
  waitToolEnabled?: boolean;
404
+ waitToolDefaultTimeoutMs?: number;
403
405
  handleScheduledRunAction?: (params: SubagentParamsLike, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
404
406
  watchdog?: MainWatchdogRuntime;
405
407
  tempArtifactsDir: string;
406
408
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
407
409
  expandTilde: (p: string) => string;
408
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig; maxThinking?: AgentConfig["maxThinking"]; cwd?: string; scope?: AgentScope; directories?: UnknownAgentDiagnosticContext["directories"] };
410
+ discoverAgents: (cwd: string, scope: AgentScope, preferredModelProvider?: string) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig; maxThinking?: AgentConfig["maxThinking"]; cwd?: string; scope?: AgentScope; directories?: UnknownAgentDiagnosticContext["directories"] };
409
411
  allowMutatingManagementActions?: boolean;
410
412
  activateSupervisorTransport?: () => void;
411
413
  refreshResultDelivery?: () => void;
@@ -447,7 +449,6 @@ interface ExecutionContextData {
447
449
  deadlineAt?: number;
448
450
  /** Raw global config.toolTimeoutMs, for per-step resolution in async runners. */
449
451
  configToolTimeoutMs?: number;
450
- turnBudget?: ResolvedTurnBudget;
451
452
  toolBudget?: ResolvedToolBudget;
452
453
  usageBudget?: UsageBudgetConfig;
453
454
  allowZeroToolBudget?: boolean;
@@ -504,12 +505,13 @@ function getForegroundControl(state: SubagentState, runId: string | undefined) {
504
505
  return newest;
505
506
  }
506
507
 
507
- function promptAuditRedoParams(value: unknown, rewrittenTask: string): SubagentParamsLike {
508
+ export function promptAuditRedoParams(value: unknown, rewrittenTask: string): SubagentParamsLike {
508
509
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Prompt redo is missing a safe live launch contract.");
509
510
  const params = { ...(value as SubagentParamsLike), task: rewrittenTask, async: false };
510
511
  delete params.workflowParentRunId;
511
512
  delete params.workflowKey;
512
513
  delete params.workflowChildAsyncId;
514
+ delete params.workflowAwaitDetached;
513
515
  delete params.workflowParentDeadlineAt;
514
516
  delete params.suppressRoutineResultIntercom;
515
517
  if (params.worktree === true && Array.isArray(params.tasks)) delete params.cwd;
@@ -700,6 +702,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
700
702
  const child = {
701
703
  agent: result.agent,
702
704
  index,
705
+ ...(result.sessionName ? { sessionName: result.sessionName } : {}),
703
706
  ...(result.context ? { context: result.context } : {}),
704
707
  status: resolveSubagentResultStatus(omitUndefinedProperties({
705
708
  exitCode: result.exitCode,
@@ -1254,7 +1257,7 @@ function appendStepToAsyncChain(input: {
1254
1257
  }
1255
1258
 
1256
1259
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
1257
- const discoveredForAppend = input.deps.discoverAgents(input.requestCwd, scope);
1260
+ const discoveredForAppend = input.deps.discoverAgents(input.requestCwd, scope, input.parentModel?.provider);
1258
1261
  const agents = discoveredForAppend.agents;
1259
1262
  const contextPolicy = resolveExplicitContextPolicy(input.params);
1260
1263
  const chainSkillInput = normalizeSkillInput(input.params.skill);
@@ -1284,6 +1287,7 @@ function appendStepToAsyncChain(input: {
1284
1287
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
1285
1288
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1286
1289
  waitToolEnabled: input.deps.waitToolEnabled,
1290
+ waitToolDefaultTimeoutMs: input.deps.waitToolDefaultTimeoutMs,
1287
1291
  contextForAgent: contextPolicy.contextForAgent,
1288
1292
  asyncDir: resolved.location.asyncDir,
1289
1293
  validateOutputBindings: false,
@@ -1654,6 +1658,7 @@ async function resumeExternalJobFollowUp(input: {
1654
1658
  ...(input.parentSessionFile ? { sessionRoot: input.deps.getSubagentSessionRoot(input.parentSessionFile) } : {}),
1655
1659
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1656
1660
  waitToolEnabled: input.deps.waitToolEnabled,
1661
+ waitToolDefaultTimeoutMs: input.deps.waitToolDefaultTimeoutMs,
1657
1662
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1658
1663
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1659
1664
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
@@ -1772,7 +1777,7 @@ async function resumeAsyncRun(input: {
1772
1777
  input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
1773
1778
  const effectiveCwd = target.cwd ?? input.requestCwd;
1774
1779
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
1775
- const discovered = input.deps.discoverAgents(effectiveCwd, scope);
1780
+ const discovered = input.deps.discoverAgents(effectiveCwd, scope, input.parentModel?.provider);
1776
1781
  const discoveredAgents = discovered.agents;
1777
1782
  const unknownAgentDiagnosticContext = diagnosticContextFromDiscovery(discovered, effectiveCwd, scope);
1778
1783
  const modelScope = discovered.modelScope;
@@ -1909,6 +1914,7 @@ async function resumeAsyncRun(input: {
1909
1914
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
1910
1915
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1911
1916
  waitToolEnabled: input.deps.waitToolEnabled,
1917
+ waitToolDefaultTimeoutMs: input.deps.waitToolDefaultTimeoutMs,
1912
1918
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1913
1919
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1914
1920
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
@@ -2020,10 +2026,12 @@ async function resumeAsyncRun(input: {
2020
2026
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
2021
2027
  maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
2022
2028
  waitToolEnabled: input.deps.waitToolEnabled,
2029
+ waitToolDefaultTimeoutMs: input.deps.waitToolDefaultTimeoutMs,
2023
2030
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
2024
2031
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
2025
2032
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
2026
2033
  worktree: input.params.worktree === true,
2034
+ lane: input.params.lane ?? recoveryDescriptor?.lane,
2027
2035
  controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
2028
2036
  intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
2029
2037
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
@@ -2037,7 +2045,6 @@ async function resumeAsyncRun(input: {
2037
2045
  ...(input.params.acceptance !== undefined ? { acceptance: input.params.acceptance } : foregroundContract?.acceptance !== undefined ? { acceptance: foregroundContract.acceptance } : recoveryDescriptor?.acceptance !== undefined ? { acceptance: recoveryDescriptor.acceptance } : {}),
2038
2046
  ...(input.params.timeoutMs !== undefined ? { timeoutMs: input.params.timeoutMs } : {}),
2039
2047
  ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
2040
- ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
2041
2048
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
2042
2049
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
2043
2050
  runFanoutBudget: input.params.runFanoutBudget ?? recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
@@ -2085,20 +2092,14 @@ async function resumeAsyncRun(input: {
2085
2092
  input.signal?.removeEventListener("abort", stopOnAbort);
2086
2093
  }
2087
2094
  fs.rmSync(resultPath, { force: true });
2088
- const totalCost = completed.totalCost;
2095
+ const usage = importedAsyncRootUsage(completed);
2089
2096
  const childResult: SingleResult = {
2090
2097
  index: 0,
2091
2098
  agent: completed.agent,
2099
+ ...(completed.sessionName ? { sessionName: completed.sessionName } : {}),
2092
2100
  task: effectiveFollowUp,
2093
2101
  exitCode: completed.exitCode,
2094
- usage: {
2095
- input: totalCost?.inputTokens ?? 0,
2096
- output: totalCost?.outputTokens ?? 0,
2097
- cacheRead: 0,
2098
- cacheWrite: 0,
2099
- cost: totalCost?.costUsd ?? 0,
2100
- turns: 0,
2101
- },
2102
+ usage,
2102
2103
  finalOutput: completed.output,
2103
2104
  outputState: completed.output.trim() ? "present" : "absent",
2104
2105
  ...(completed.error ? { error: completed.error } : {}),
@@ -2235,6 +2236,7 @@ async function emitForegroundResultIntercom(input: {
2235
2236
  if (!input.intercomBridge.active || !input.intercomBridge.resultDelivery || !input.intercomBridge.orchestratorTarget) return null;
2236
2237
  const children = input.results.flatMap((result, index) => result.detached ? [] : [omitUndefinedProperties({
2237
2238
  agent: result.agent,
2239
+ ...(result.sessionName ? { sessionName: result.sessionName } : {}),
2238
2240
  status: foregroundResultIntercomStatus(result),
2239
2241
  outputState: result.outputState ?? "unknown",
2240
2242
  summary: resultSummaryForIntercom(result),
@@ -2647,9 +2649,6 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
2647
2649
  ? { timeoutMs: agent.defaultTimeoutMs }
2648
2650
  : {}),
2649
2651
  ...(parentTimeoutMs !== undefined ? { timeoutMs: parentTimeoutMs } : {}),
2650
- ...(params.turnBudget === undefined && agent.defaultTurnBudget !== undefined
2651
- ? { turnBudget: agent.defaultTurnBudget }
2652
- : {}),
2653
2652
  ...(params.acceptance === undefined && agent.defaultAcceptance !== undefined
2654
2653
  ? { acceptance: agent.defaultAcceptance }
2655
2654
  : {}),
@@ -2818,6 +2817,14 @@ function withResolvedContext(
2818
2817
  };
2819
2818
  }
2820
2819
 
2820
+ function withAggregatedToolUsage(result: AgentToolResult<Details>): AgentToolResult<Details> {
2821
+ if (result.details.results.length === 0) return result;
2822
+ const usage = sumResultsUsage(result.details.results);
2823
+ return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
2824
+ ? { ...result, usage: toAgentToolUsage(usage) }
2825
+ : result;
2826
+ }
2827
+
2821
2828
  function withForkThinkingNotes(
2822
2829
  result: AgentToolResult<Details>,
2823
2830
  downgrades: Map<number, string>,
@@ -2843,66 +2850,6 @@ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown, cont
2843
2850
  );
2844
2851
  }
2845
2852
 
2846
- function collectChainSessionFiles(
2847
- chain: ChainStep[],
2848
- sessionFileForTask: ForkSessionFileForTask,
2849
- dynamicFanoutMaxItems?: number,
2850
- ): (string | undefined)[] {
2851
- const sessionFiles: (string | undefined)[] = [];
2852
- let flatIndex = 0;
2853
- for (const step of chain) {
2854
- if (isParallelStep(step)) {
2855
- for (const task of step.parallel) {
2856
- sessionFiles.push(sessionFileForTask(task.agent, flatIndex, task.model));
2857
- flatIndex++;
2858
- }
2859
- continue;
2860
- }
2861
- if (isDynamicParallelStep(step)) {
2862
- const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
2863
- for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
2864
- sessionFiles.push(sessionFileForTask(step.parallel.agent, flatIndex, step.parallel.model));
2865
- flatIndex++;
2866
- }
2867
- continue;
2868
- }
2869
- const sequential = step as SequentialStep;
2870
- sessionFiles.push(sessionFileForTask(sequential.agent, flatIndex, sequential.model));
2871
- flatIndex++;
2872
- }
2873
- return sessionFiles;
2874
- }
2875
-
2876
- function collectChainThinkingOverrides(
2877
- chain: ChainStep[],
2878
- thinkingOverrideForTask: ForkThinkingOverrideForTask,
2879
- dynamicFanoutMaxItems?: number,
2880
- ): (AgentConfig["thinking"] | undefined)[] {
2881
- const thinkingOverrides: (AgentConfig["thinking"] | undefined)[] = [];
2882
- let flatIndex = 0;
2883
- for (const step of chain) {
2884
- if (isParallelStep(step)) {
2885
- for (const task of step.parallel) {
2886
- thinkingOverrides.push(thinkingOverrideForTask(task.agent, flatIndex, task.model));
2887
- flatIndex++;
2888
- }
2889
- continue;
2890
- }
2891
- if (isDynamicParallelStep(step)) {
2892
- const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
2893
- for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
2894
- thinkingOverrides.push(thinkingOverrideForTask(step.parallel.agent, flatIndex, step.parallel.model));
2895
- flatIndex++;
2896
- }
2897
- continue;
2898
- }
2899
- const sequential = step as SequentialStep;
2900
- thinkingOverrides.push(thinkingOverrideForTask(sequential.agent, flatIndex, sequential.model));
2901
- flatIndex++;
2902
- }
2903
- return thinkingOverrides;
2904
- }
2905
-
2906
2853
  type StaticLaunchSummary = { agent: string; model?: string; thinking?: string };
2907
2854
 
2908
2855
  function resolveStaticLaunchSummary(input: {
@@ -2988,14 +2935,6 @@ function collectStaticLaunchSummaries(input: {
2988
2935
  return input.params.agent ? [summary(input.params.agent, 0, input.params.model as string | undefined)] : [];
2989
2936
  }
2990
2937
 
2991
- function firstChainAgent(chain: ChainStep[]): string | undefined {
2992
- const first = chain[0];
2993
- if (!first) return undefined;
2994
- if (isParallelStep(first)) return first.parallel[0]?.agent;
2995
- if (isDynamicParallelStep(first)) return first.parallel.agent;
2996
- return (first as SequentialStep).agent;
2997
- }
2998
-
2999
2938
  function firstRawChainTask(chain: ChainStep[]): string | undefined {
3000
2939
  const first = chain[0];
3001
2940
  if (!first) return undefined;
@@ -3087,6 +3026,18 @@ async function preflightForkSessionsForStaticTasks(
3087
3026
  }
3088
3027
  }
3089
3028
 
3029
+ function importedAsyncRootUsage(completed: Awaited<ReturnType<typeof waitForImportedAsyncRoot>>): Usage {
3030
+ const totalCost = completed.totalCost;
3031
+ return completed.usage ?? {
3032
+ input: totalCost?.inputTokens ?? 0,
3033
+ output: totalCost?.outputTokens ?? 0,
3034
+ cacheRead: 0,
3035
+ cacheWrite: 0,
3036
+ cost: totalCost?.costUsd ?? 0,
3037
+ turns: 0,
3038
+ };
3039
+ }
3040
+
3090
3041
  async function waitForWorkflowAsyncSingleResult(
3091
3042
  params: SubagentParamsLike,
3092
3043
  launchResult: AgentToolResult<Details>,
@@ -3108,20 +3059,14 @@ async function waitForWorkflowAsyncSingleResult(
3108
3059
  options.signal?.removeEventListener("abort", stopOnAbort);
3109
3060
  }
3110
3061
  fs.rmSync(resultPath, { force: true });
3111
- const totalCost = completed.totalCost;
3062
+ const usage = importedAsyncRootUsage(completed);
3112
3063
  const childResult: SingleResult = omitUndefinedProperties({
3113
3064
  index: 0,
3114
3065
  agent: completed.agent,
3066
+ ...(completed.sessionName ? { sessionName: completed.sessionName } : {}),
3115
3067
  task: options.task,
3116
3068
  exitCode: completed.exitCode,
3117
- usage: {
3118
- input: totalCost?.inputTokens ?? 0,
3119
- output: totalCost?.outputTokens ?? 0,
3120
- cacheRead: 0,
3121
- cacheWrite: 0,
3122
- cost: totalCost?.costUsd ?? 0,
3123
- turns: 0,
3124
- },
3069
+ usage,
3125
3070
  finalOutput: completed.output,
3126
3071
  outputState: completed.output.trim() ? "present" as const : "absent" as const,
3127
3072
  ...(completed.error ? { error: completed.error } : {}),
@@ -3160,7 +3105,6 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3160
3105
  ctx,
3161
3106
  shareEnabled,
3162
3107
  sessionRoot,
3163
- sessionFileForIndex,
3164
3108
  sessionFileForTask,
3165
3109
  thinkingOverrideForTask,
3166
3110
  artifactConfig,
@@ -3252,6 +3196,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3252
3196
  skills,
3253
3197
  output: effectiveOutput,
3254
3198
  outputMode: effectiveOutputMode,
3199
+ outputClaimPath: params.workflowOutputClaimPath,
3255
3200
  ...(params.reads !== undefined ? { reads: params.reads } : {}),
3256
3201
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
3257
3202
  modelOverride,
@@ -3261,6 +3206,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3261
3206
  thinkingCeiling: a.maxThinking,
3262
3207
  maxSubagentDepth,
3263
3208
  waitToolEnabled: deps.waitToolEnabled,
3209
+ waitToolDefaultTimeoutMs: deps.waitToolDefaultTimeoutMs,
3264
3210
  ...(params.worktree === true ? { worktree: true } : {}),
3265
3211
  worktreeSetupHook: deps.config.worktreeSetupHook,
3266
3212
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -3275,7 +3221,6 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3275
3221
  extensionBindings: params.extensionBindings,
3276
3222
  acceptance: params.acceptance,
3277
3223
  timeoutMs: data.timeoutMs,
3278
- turnBudget: data.turnBudget,
3279
3224
  toolBudget: data.toolBudget,
3280
3225
  usageBudget: data.usageBudget,
3281
3226
  configToolBudget: data.configToolBudget,
@@ -3285,6 +3230,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3285
3230
  runFanoutBudget: data.runFanoutBudget,
3286
3231
  parentWorkflowRunId: params.workflowParentRunId,
3287
3232
  workflowKey: params.workflowKey,
3233
+ lane: params.lane,
3288
3234
  workflowAwaitAsync: params.workflowAwaitAsync,
3289
3235
  }));
3290
3236
  return waitForWorkflowAsyncSingleResult(params, asyncResult, { runId: id, task: params.task ?? "", signal: data.signal, state: deps.state, kill: deps.kill });
@@ -3301,6 +3247,7 @@ function createSingleWorktreeSetup(
3301
3247
  setupHook: ExtensionConfig["worktreeSetupHook"],
3302
3248
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
3303
3249
  baseDir: ExtensionConfig["worktreeBaseDir"],
3250
+ beforeCreate?: (setup: WorktreeSetup) => void,
3304
3251
  ): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
3305
3252
  if (!enabled) return {};
3306
3253
  try {
@@ -3311,6 +3258,7 @@ function createSingleWorktreeSetup(
3311
3258
  ? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
3312
3259
  : undefined,
3313
3260
  baseDir,
3261
+ beforeCreate,
3314
3262
  })),
3315
3263
  };
3316
3264
  } catch (error) {
@@ -3353,10 +3301,30 @@ function workflowChildDefaultOutput(aggregateOutputPath: string | undefined, art
3353
3301
  return path.join(artifactsDir, "outputs", sanitizeRunPathSegment(workflowRunId), `${workflowKey}.md`);
3354
3302
  }
3355
3303
 
3304
+ function workflowHostCommandRunner(input: {
3305
+ workflowCwd: string;
3306
+ artifactsDir: string;
3307
+ workflowRunId: string;
3308
+ claimedOutputPaths: Map<string, string>;
3309
+ producedOutputPaths: Set<string>;
3310
+ }) {
3311
+ return async (key: string, params: WorkflowHostCommandParams, signal: AbortSignal): Promise<WorkflowHostCommandResult> => {
3312
+ const defaultOutputPath = path.join(input.artifactsDir, "outputs", sanitizeRunPathSegment(input.workflowRunId), "host", `${sanitizeRunPathSegment(key)}.log`);
3313
+ const outputPath = params.output ? path.resolve(input.workflowCwd, params.output) : defaultOutputPath;
3314
+ const claimPath = resolveWorkflowHostOutputClaimPath(outputPath);
3315
+ const previous = input.claimedOutputPaths.get(claimPath);
3316
+ if (previous) throw new Error(`runs.host('${key}') output path is already claimed by '${previous}': ${outputPath}.`);
3317
+ input.claimedOutputPaths.set(claimPath, `host:${key}`);
3318
+ const result = await executeWorkflowHostCommand({ key, params, cwd: input.workflowCwd, defaultOutputPath, claimedOutputPath: claimPath, signal });
3319
+ input.producedOutputPaths.add(resolveWorkflowHostOutputClaimPath(result.outputPath));
3320
+ return result;
3321
+ };
3322
+ }
3323
+
3356
3324
  function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string, producedChildOutputPaths: ReadonlySet<string>): string | undefined {
3357
3325
  if (!outputPath) return undefined;
3358
3326
  try {
3359
- if (producedChildOutputPaths.has(outputPath)) return undefined;
3327
+ if (producedChildOutputPaths.has(resolveWorkflowHostOutputClaimPath(outputPath))) return undefined;
3360
3328
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
3361
3329
  fs.writeFileSync(outputPath, text, "utf-8");
3362
3330
  return undefined;
@@ -3427,7 +3395,7 @@ function workflowChildOutputClaims(input: {
3427
3395
  state: SubagentState;
3428
3396
  claimedOutputPaths: Map<string, string>;
3429
3397
  entries: Array<{ key: string; params: Record<string, unknown> }>;
3430
- }): { error?: string; claims?: Map<string, string>; overrides?: Map<string, string> } {
3398
+ }): { error?: string; claims?: Map<string, string>; childClaims?: Map<string, string>; overrides?: Map<string, string> } {
3431
3399
  const resolvedEntries = input.entries.map(({ key, params }) => ({
3432
3400
  key,
3433
3401
  ...resolveWorkflowChildOutputPath({ ...input, key, params }),
@@ -3435,24 +3403,30 @@ function workflowChildOutputClaims(input: {
3435
3403
  const paths = new Map<string, number>();
3436
3404
  for (const claimedPath of input.claimedOutputPaths.keys()) paths.set(claimedPath, 1);
3437
3405
  for (const { path: resolved } of resolvedEntries) {
3438
- if (resolved) paths.set(resolved, (paths.get(resolved) ?? 0) + 1);
3406
+ if (resolved) {
3407
+ const claimPath = resolveWorkflowHostOutputClaimPath(resolved);
3408
+ paths.set(claimPath, (paths.get(claimPath) ?? 0) + 1);
3409
+ }
3439
3410
  }
3440
3411
  const overrides = new Map<string, string>();
3441
3412
  for (const entry of resolvedEntries) {
3442
- if (entry.inherited && entry.path && (paths.get(entry.path) ?? 0) > 1) {
3413
+ if (entry.inherited && entry.path && (paths.get(resolveWorkflowHostOutputClaimPath(entry.path)) ?? 0) > 1) {
3443
3414
  const output = workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, entry.key);
3444
3415
  overrides.set(entry.key, output);
3445
3416
  entry.path = output;
3446
3417
  }
3447
3418
  }
3448
3419
  const claims = new Map(input.claimedOutputPaths);
3420
+ const childClaims = new Map<string, string>();
3449
3421
  for (const { key, path: resolved } of resolvedEntries) {
3450
3422
  if (!resolved) continue;
3451
- const previous = claims.get(resolved);
3423
+ const claimPath = resolveWorkflowHostOutputClaimPath(resolved);
3424
+ const previous = claims.get(claimPath);
3452
3425
  if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
3453
- claims.set(resolved, key);
3426
+ claims.set(claimPath, key);
3427
+ childClaims.set(key, claimPath);
3454
3428
  }
3455
- return { claims, overrides };
3429
+ return { claims, childClaims, overrides };
3456
3430
  }
3457
3431
 
3458
3432
  function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
@@ -3474,12 +3448,14 @@ function prepareWorkflowChildLaunchParams(input: {
3474
3448
  agents: AgentConfig[];
3475
3449
  workflowAgentScope?: unknown;
3476
3450
  outputOverride?: string;
3477
- options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number; capabilityCeiling?: ResolvedSubagentCapabilityCeiling };
3451
+ outputClaimPath?: string;
3452
+ options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; awaitDetachedChild?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number; capabilityCeiling?: ResolvedSubagentCapabilityCeiling };
3478
3453
  }): SubagentParamsLike {
3479
3454
  let childParams = input.childParams;
3480
- if (input.childParams.output === undefined && input.childParams.resume === undefined && input.outputOverride !== undefined) {
3455
+ const usesDefaultOutput = input.childParams.output === undefined && input.childParams.resume === undefined;
3456
+ if (usesDefaultOutput && input.outputOverride !== undefined) {
3481
3457
  childParams = { ...input.childParams, output: input.outputOverride };
3482
- } else if (input.childParams.output === undefined && input.childParams.resume === undefined && input.aggregateOutputPath !== undefined) {
3458
+ } else if (usesDefaultOutput && input.aggregateOutputPath !== undefined) {
3483
3459
  childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3484
3460
  } else if (input.childParams.resume === undefined) {
3485
3461
  const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, agents: input.agents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
@@ -3492,7 +3468,7 @@ function prepareWorkflowChildLaunchParams(input: {
3492
3468
  ? resolveAgentName(childParams.agent, discoveredAgents).agent ?? resolveAgentName(childParams.agent, input.agents).agent
3493
3469
  : undefined;
3494
3470
  const externalAsyncRequired = agent?.runner?.type === "external-cli" || agent?.runner?.type === "external-job";
3495
- return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, { ...input.options, externalAsyncRequired });
3471
+ return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, { ...input.options, externalAsyncRequired, outputClaimPath: input.outputClaimPath });
3496
3472
  }
3497
3473
 
3498
3474
  function finalizeSingleWorktreeHandoff(input: {
@@ -3502,6 +3478,8 @@ function finalizeSingleWorktreeHandoff(input: {
3502
3478
  cwd: string;
3503
3479
  agent: string;
3504
3480
  result: SingleResult;
3481
+ workflowKey?: string;
3482
+ lane?: import("../../shared/types.ts").WorkflowLaneMetadata;
3505
3483
  }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
3506
3484
  const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
3507
3485
  const diffs = diffWorktrees(input.worktreeSetup, [input.agent], diffsDir);
@@ -3516,9 +3494,13 @@ function finalizeSingleWorktreeHandoff(input: {
3516
3494
  stepIndex: 0,
3517
3495
  flatStartIndex: 0,
3518
3496
  setup: input.worktreeSetup,
3497
+ laneBindings: input.workflowKey || input.lane ? [{ index: 0, taskIndex: 0, ...(input.workflowKey ? { workflowKey: input.workflowKey } : {}), runId: input.workflowKey ? input.runId : undefined, ...(input.lane ? { lane: input.lane } : {}) }] : undefined,
3519
3498
  diffs,
3520
3499
  results: [{
3521
3500
  agent: input.result.agent,
3501
+ ...(input.workflowKey ? { workflowKey: input.workflowKey } : {}),
3502
+ ...(input.workflowKey ? { runId: input.runId } : {}),
3503
+ ...(input.lane ? { lane: input.lane } : {}),
3522
3504
  status: resolveSubagentResultStatus(omitUndefinedProperties({
3523
3505
  exitCode: input.result.exitCode,
3524
3506
  interrupted: input.result.interrupted,
@@ -3564,10 +3546,16 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3564
3546
  artifactConfig,
3565
3547
  artifactsDir,
3566
3548
  onUpdate,
3567
- sessionRoot,
3568
3549
  controlConfig,
3569
3550
  contextPolicy,
3570
3551
  } = data;
3552
+ let lane: import("../../shared/types.ts").WorkflowLaneMetadata | undefined;
3553
+ try {
3554
+ lane = normalizeWorkflowLaneMetadata(params.lane, "lane");
3555
+ assertWorkflowLaneKey(lane, params.workflowKey, "lane");
3556
+ } catch (error) {
3557
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
3558
+ }
3571
3559
  const onControlEvent = createForegroundControlNotifier(data, deps);
3572
3560
  const childBridgeActive = intercomBridgeAppliesToAgent(data.intercomBridge, contextPolicy, params.agent!);
3573
3561
  const childIntercomTarget = childBridgeActive ? resolveSubagentIntercomTarget(runId, params.agent!, 0) : undefined;
@@ -3588,7 +3576,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3588
3576
  const currentProvider = parentModel?.provider;
3589
3577
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
3590
3578
  const modelScopes = resolveModelScopesForAgent(data.modelScope, agentConfig.name, parentModel);
3591
- let task = params.task ?? "";
3579
+ let task = typeof params.task === "string" ? params.task : "";
3592
3580
  let modelOverride: string | undefined = resolveEffectiveSubagentModel(
3593
3581
  params.model as string | undefined,
3594
3582
  agentConfig.model,
@@ -3608,6 +3596,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3608
3596
 
3609
3597
 
3610
3598
  const sourceCwd = effectiveCwd;
3599
+ let pendingHandoff: Details["parallelHandoff"];
3611
3600
  const { setup: worktreeSetup, errorResult: worktreeSetupError } = createSingleWorktreeSetup(
3612
3601
  params.worktree,
3613
3602
  sourceCwd,
@@ -3616,12 +3605,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3616
3605
  deps.config.worktreeSetupHook,
3617
3606
  deps.config.worktreeSetupHookTimeoutMs,
3618
3607
  deps.config.worktreeBaseDir,
3619
- );
3620
- if (worktreeSetupError) return worktreeSetupError;
3621
- const singleCwd = worktreeSetup?.worktrees[0]?.agentCwd ?? sourceCwd;
3622
- let pendingHandoff: Details["parallelHandoff"];
3623
- if (worktreeSetup) {
3624
- try {
3608
+ (plannedSetup) => {
3625
3609
  pendingHandoff = writePendingParallelHandoff({
3626
3610
  manifestPath: parallelHandoffPath(artifactsDir, runId),
3627
3611
  runId,
@@ -3630,13 +3614,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3630
3614
  cwd: sourceCwd,
3631
3615
  stepIndex: 0,
3632
3616
  flatStartIndex: 0,
3633
- setup: worktreeSetup,
3617
+ setup: plannedSetup,
3618
+ laneBindings: params.workflowKey || lane ? [{ index: 0, taskIndex: 0, ...(params.workflowKey ? { workflowKey: params.workflowKey, runId } : {}), ...(lane ? { lane } : {}) }] : undefined,
3634
3619
  });
3635
- } catch (error) {
3636
- cleanupWorktrees(worktreeSetup);
3637
- return { content: [{ type: "text", text: formatParallelHandoffError(error) }], isError: true, details: { mode: "single", results: [] } };
3638
- }
3639
- }
3620
+ },
3621
+ );
3622
+ if (worktreeSetupError) return worktreeSetupError;
3623
+ const singleCwd = worktreeSetup?.worktrees[0]?.agentCwd ?? sourceCwd;
3640
3624
 
3641
3625
  const authoredTask = task;
3642
3626
  if (shouldForkAgent(contextPolicy, params.agent!)) {
@@ -3702,8 +3686,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3702
3686
 
3703
3687
  const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3704
3688
  let r: Awaited<ReturnType<typeof runSync>> | undefined;
3689
+ let resolveDetachedWorkflowChild: ((result: Awaited<ReturnType<typeof runSync>>) => void) | undefined;
3690
+ const detachedWorkflowChild = params.workflowAwaitDetached === true
3691
+ ? new Promise<Awaited<ReturnType<typeof runSync>>>((resolve) => { resolveDetachedWorkflowChild = resolve; })
3692
+ : undefined;
3705
3693
  try {
3706
- r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3694
+ const launched = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3707
3695
  permissions: deps.config.permissions,
3708
3696
  runtimeSnapshotHost: deps.pi,
3709
3697
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
@@ -3726,9 +3714,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3726
3714
  artifactConfig,
3727
3715
  maxOutput: params.maxOutput,
3728
3716
  outputPath,
3717
+ outputClaimPath: params.workflowOutputClaimPath,
3729
3718
  outputMode: effectiveOutputMode,
3730
3719
  maxSubagentDepth,
3731
3720
  waitToolEnabled: deps.waitToolEnabled,
3721
+ waitToolDefaultTimeoutMs: deps.waitToolDefaultTimeoutMs,
3732
3722
  onUpdate: forwardSingleUpdate,
3733
3723
  controlConfig,
3734
3724
  onControlEvent,
@@ -3756,9 +3746,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3756
3746
  detachForeground = detach;
3757
3747
  },
3758
3748
  onDetachedExit: (result) => {
3749
+ if (resolveDetachedWorkflowChild) {
3750
+ resolveDetachedWorkflowChild(result);
3751
+ return;
3752
+ }
3759
3753
  try {
3760
3754
  if (worktreeSetup) {
3761
- finalizeSingleWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: sourceCwd, agent: params.agent!, result });
3755
+ finalizeSingleWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: sourceCwd, agent: params.agent!, result, workflowKey: params.workflowKey, lane });
3762
3756
  }
3763
3757
  try {
3764
3758
  updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: true });
@@ -3793,12 +3787,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3793
3787
  deadlineAt,
3794
3788
  toolTimeoutMs: params.toolTimeoutMs,
3795
3789
  configToolTimeoutMs: data.configToolTimeoutMs,
3796
- turnBudget: data.turnBudget,
3797
- enforceHardTurnLimit: params.enforceHardTurnLimit,
3798
3790
  toolBudget: effectiveToolBudget.toolBudget,
3799
3791
  capabilityCeiling: data.capabilityCeiling,
3800
3792
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3801
3793
  }));
3794
+ r = launched.detached && detachedWorkflowChild ? await detachedWorkflowChild : launched;
3802
3795
  } catch (error) {
3803
3796
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3804
3797
  throw error;
@@ -3819,7 +3812,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3819
3812
  if (worktreeSetup) {
3820
3813
  worktreeHandoff = r.detached
3821
3814
  ? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
3822
- : finalizeSingleWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: sourceCwd, agent: params.agent!, result: r });
3815
+ : finalizeSingleWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: sourceCwd, agent: params.agent!, result: r, workflowKey: params.workflowKey, lane });
3823
3816
  }
3824
3817
 
3825
3818
  if (r.progress) allProgress.push(r.progress);
@@ -3848,7 +3841,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3848
3841
  runId,
3849
3842
  timeoutMs: data.timeoutMs,
3850
3843
  results: [r],
3851
- ...(data.turnBudget ? { turnBudget: data.turnBudget } : {}),
3852
3844
  ...(effectiveToolBudget.toolBudget ? { toolBudget: effectiveToolBudget.toolBudget } : {}),
3853
3845
  progress: params.includeProgress ? allProgress : undefined,
3854
3846
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
@@ -3933,7 +3925,7 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
3933
3925
  };
3934
3926
  }
3935
3927
 
3936
- const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
3928
+ const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionName?: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
3937
3929
 
3938
3930
  /**
3939
3931
  * Terminal-mission retention can remove a mission while its children still
@@ -4005,7 +3997,7 @@ export function bindMissionWorkflowChildAsyncLaunch(
4005
3997
  function workflowChildResult(
4006
3998
  key: string,
4007
3999
  result: AgentToolResult<Details>,
4008
- childParams: Record<string, unknown> = {},
4000
+ childParams: SubagentParamsLike | Record<string, unknown> = {},
4009
4001
  resumeState?: SubagentState,
4010
4002
  forcedTerminalOutcome?: WorkflowTerminalOutcome,
4011
4003
  ): WorkflowScriptChildResult {
@@ -4066,9 +4058,12 @@ function workflowChildResult(
4066
4058
  const externalProcess = externalResult?.externalProcess ?? externalStatusStep?.externalProcess;
4067
4059
  const externalAdapter = externalRunner ? externalCliReceiptMetadata({ runner: externalRunner, externalProcess, outputReference }) : undefined;
4068
4060
  if (externalAdapter) resumability = { state: "not-resumable", reason: externalAdapter.nonResumableReason };
4061
+ const lane = normalizeWorkflowLaneMetadata(childParams.lane, `workflow child '${key}'.lane`);
4062
+ assertWorkflowLaneKey(lane, key, `workflow child '${key}'.lane`);
4069
4063
  return {
4070
4064
  key,
4071
4065
  ok,
4066
+ ...(lane ? { lane } : {}),
4072
4067
  ...(terminalOutcome ? { terminalOutcome } : {}),
4073
4068
  ...(resolvedAgents.length === 1 ? { agent: resolvedAgents[0] } : {}),
4074
4069
  ...(runId ? { runId } : {}),
@@ -4090,6 +4085,16 @@ function workflowChildResult(
4090
4085
  };
4091
4086
  }
4092
4087
 
4088
+ function workflowChildAccountingFields(child: WorkflowScriptChildResult): { usage?: Usage; sessionFile?: string } {
4089
+ if (!child.results?.length) return {};
4090
+ const usage = sumResultsUsage(child.results);
4091
+ const sessionFile = child.results.find((result) => result.sessionFile)?.sessionFile;
4092
+ return {
4093
+ ...(usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0 ? { usage } : {}),
4094
+ ...(sessionFile ? { sessionFile } : {}),
4095
+ };
4096
+ }
4097
+
4093
4098
  function workflowOutputPathMappingSummary(children: WorkflowScriptChildResult[]): string {
4094
4099
  const mappings = children.flatMap((child) => child.outputPathMapping
4095
4100
  ? [`'${child.key}': requested ${child.outputPathMapping.requestedPath} -> saved ${child.outputPathMapping.savedPath}`]
@@ -4114,21 +4119,97 @@ function workflowSteerReceipt(key: string, result: AgentToolResult<Details>): Wo
4114
4119
  };
4115
4120
  }
4116
4121
 
4122
+ const MAX_WORKFLOW_RESUME_HINT_BYTES = 1024;
4123
+ const MAX_WORKFLOW_CHILD_RUN_ID_BYTES = 256;
4124
+ const WORKFLOW_RESUME_HINT_PARENT_STATES = new Set(["complete", "failed", "partial"]);
4125
+ const WORKFLOW_STEP_STATES = new Set(["pending", "running", "complete", "completed", "failed", "partial", "paused", "stopped", "rejected"]);
4126
+
4127
+ function isSafeWorkflowChildRunId(value: unknown): value is string {
4128
+ if (typeof value !== "string") return false;
4129
+ const runId = value.trim();
4130
+ return value === runId
4131
+ && Boolean(runId)
4132
+ && Buffer.byteLength(runId, "utf8") <= MAX_WORKFLOW_CHILD_RUN_ID_BYTES
4133
+ && path.basename(runId) === runId
4134
+ && path.win32.basename(runId) === runId
4135
+ && !/[\\/]/.test(runId)
4136
+ && !runId.includes("..")
4137
+ && !/[\u0000-\u001f\u007f]/.test(runId);
4138
+ }
4139
+
4140
+ function isMissingWorkflowReceiptDiagnostic(error: unknown, workflowRunId: string): error is Error {
4141
+ return error instanceof Error
4142
+ && error.message.startsWith(`Workflow receipt '${workflowRunId}' is not available because the workflow may still be active or terminal receipt writing failed.`);
4143
+ }
4144
+
4145
+ function missingWorkflowReceiptResumeHint(reference: WorkflowReceiptResumeReference, state: SubagentState): string | undefined {
4146
+ try {
4147
+ if (!state.currentSessionId) return undefined;
4148
+ const workflowRunId = reference.workflowRunId.trim();
4149
+ const workflowStatus = readStatus(path.join(DIRS.async, workflowRunId));
4150
+ if (!workflowStatus
4151
+ || workflowStatus.runId !== workflowRunId
4152
+ || workflowStatus.mode !== "workflow"
4153
+ || workflowStatus.sessionId !== state.currentSessionId
4154
+ || typeof workflowStatus.startedAt !== "number"
4155
+ || !Number.isFinite(workflowStatus.startedAt)
4156
+ || !WORKFLOW_RESUME_HINT_PARENT_STATES.has(workflowStatus.state)
4157
+ || !Array.isArray(workflowStatus.steps)
4158
+ || workflowStatus.steps.some((step) => {
4159
+ if (!step || typeof step !== "object" || Array.isArray(step)) return true;
4160
+ const record = step as Record<string, unknown>;
4161
+ return typeof record.agent !== "string"
4162
+ || typeof record.status !== "string"
4163
+ || !WORKFLOW_STEP_STATES.has(record.status)
4164
+ || (record.workflowKey !== undefined && typeof record.workflowKey !== "string")
4165
+ || (record.runId !== undefined && typeof record.runId !== "string");
4166
+ })) return undefined;
4167
+ const matchingSteps = workflowStatus.steps.filter((step) => step.workflowKey === reference.key);
4168
+ if (matchingSteps.length !== 1) return undefined;
4169
+ const childRunId = matchingSteps[0]?.runId;
4170
+ if (!isSafeWorkflowChildRunId(childRunId)) return undefined;
4171
+
4172
+ const workflowChildren = parseWorkflowChildSummary(workflowStatus.workflowChildren);
4173
+ if (workflowChildren) {
4174
+ if (workflowChildren.workflowRunId !== workflowRunId) return undefined;
4175
+ const matchingChildren = workflowChildren.children.filter((child) => child.childId === reference.key);
4176
+ if (matchingChildren.length > 1 || (workflowChildren.inventoryComplete && matchingChildren.length !== 1)) return undefined;
4177
+ if (matchingChildren.length === 1 && matchingChildren[0]?.runId !== childRunId) return undefined;
4178
+ }
4179
+
4180
+ const target = resolveResumeTarget({ id: childRunId }, state, { asyncRequireSessionFile: true, exactOnly: true });
4181
+ if (target.kind !== "revive") return undefined;
4182
+ const hint = `Direct resumable child for workflow key '${reference.key}': subagent({ action: "resume", id: ${JSON.stringify(childRunId)}, message: "..." })`;
4183
+ return Buffer.byteLength(hint, "utf8") <= MAX_WORKFLOW_RESUME_HINT_BYTES ? hint : undefined;
4184
+ } catch {
4185
+ return undefined;
4186
+ }
4187
+ }
4188
+
4117
4189
  function resolveKeyedWorkflowResume(
4118
4190
  reference: WorkflowReceiptResumeReference,
4119
4191
  state: SubagentState,
4120
4192
  ): { runId: string; runIds: string[] } {
4121
- const entry = resolveWorkflowReceiptResumeEntry({
4122
- reference,
4123
- asyncDirRoot: DIRS.async,
4124
- assertResumable(runId) {
4125
- const target = resolveResumeTarget({ id: runId }, state, { asyncRequireSessionFile: true, exactOnly: true });
4126
- if (target.kind !== "revive") throw new Error(`Workflow receipt child '${reference.key}' latest run '${runId}' is still running.`);
4127
- },
4128
- });
4129
- const runId = entry.latestRunId;
4130
- if (!runId) throw new Error(`Workflow receipt child '${reference.key}' has no retained run id.`);
4131
- return { runId, runIds: entry.continuation.runIds };
4193
+ try {
4194
+ const entry = resolveWorkflowReceiptResumeEntry({
4195
+ reference,
4196
+ asyncDirRoot: DIRS.async,
4197
+ assertResumable(runId) {
4198
+ const target = resolveResumeTarget({ id: runId }, state, { asyncRequireSessionFile: true, exactOnly: true });
4199
+ if (target.kind !== "revive") throw new Error(`Workflow receipt child '${reference.key}' latest run '${runId}' is still running.`);
4200
+ },
4201
+ });
4202
+ const runId = entry.latestRunId;
4203
+ if (!runId) throw new Error(`Workflow receipt child '${reference.key}' has no retained run id.`);
4204
+ return { runId, runIds: entry.continuation.runIds };
4205
+ } catch (error) {
4206
+ const workflowRunId = reference.workflowRunId.trim();
4207
+ if (isMissingWorkflowReceiptDiagnostic(error, workflowRunId)) {
4208
+ const hint = missingWorkflowReceiptResumeHint(reference, state);
4209
+ if (hint) throw new Error(`${error.message} ${hint}`, { cause: error });
4210
+ }
4211
+ throw error;
4212
+ }
4132
4213
  }
4133
4214
 
4134
4215
  function terminalWorkflowReceipt(
@@ -4137,8 +4218,9 @@ function terminalWorkflowReceipt(
4137
4218
  children: WorkflowScriptChildResult[],
4138
4219
  workflowChildren?: WorkflowReceipt["workflowChildren"],
4139
4220
  terminalOutcome?: WorkflowTerminalOutcome,
4221
+ hostSteps?: WorkflowReceipt["hostSteps"],
4140
4222
  ): WorkflowReceipt {
4141
- return buildWorkflowReceipt({ workflowRunId, state, children, workflowChildren, terminalOutcome });
4223
+ return buildWorkflowReceipt({ workflowRunId, state, children, workflowChildren, terminalOutcome, hostSteps });
4142
4224
  }
4143
4225
 
4144
4226
  function workflowFailureTerminalOutcome(error: unknown, _children: WorkflowScriptChildResult[], usageBudget: ReturnType<typeof usageBudgetState>): WorkflowTerminalOutcome | undefined {
@@ -4228,9 +4310,11 @@ export function prepareWorkflowLaunchParams(
4228
4310
  childParams: Record<string, unknown>,
4229
4311
  parentWorkflowRunId: string,
4230
4312
  workflowKey: string,
4231
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number; externalAsyncRequired?: boolean; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } = {},
4313
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; awaitDetachedChild?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number; externalAsyncRequired?: boolean; capabilityCeiling?: ResolvedSubagentCapabilityCeiling; outputClaimPath?: string } = {},
4232
4314
  ): SubagentParamsLike {
4233
4315
  const capabilityCeiling = intersectSubagentCapabilityCeilings(workflowDefaults.capabilityCeiling, options.capabilityCeiling);
4316
+ const lane = normalizeWorkflowLaneMetadata(Object.hasOwn(childParams, "lane") ? childParams.lane : workflowDefaults.lane, `workflow child '${workflowKey}'.lane`);
4317
+ assertWorkflowLaneKey(lane, workflowKey, `workflow child '${workflowKey}'.lane`);
4234
4318
  const parentTimeoutMs = options.parentDeadlineAt === undefined
4235
4319
  || childParams.timeoutMs !== undefined
4236
4320
  || childParams.maxRuntimeMs !== undefined
@@ -4246,7 +4330,6 @@ export function prepareWorkflowLaunchParams(
4246
4330
  throw new Error("gate is not supported with retained resume; resume uses the retained child contract.");
4247
4331
  }
4248
4332
  const timeoutMs = childParams.timeoutMs ?? childParams.maxRuntimeMs ?? workflowDefaults.timeoutMs ?? workflowDefaults.maxRuntimeMs;
4249
- const turnBudget = childParams.turnBudget ?? workflowDefaults.turnBudget;
4250
4333
  const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
4251
4334
  const intercomBridge = childParams.intercomBridge ?? workflowDefaults.intercomBridge;
4252
4335
  const worktree = childParams.worktree ?? workflowDefaults.worktree;
@@ -4266,6 +4349,7 @@ export function prepareWorkflowLaunchParams(
4266
4349
  message: typeof childParams.task === "string" ? childParams.task.trim() : "",
4267
4350
  workflowParentRunId: parentWorkflowRunId,
4268
4351
  workflowKey,
4352
+ ...(lane ? { lane } : {}),
4269
4353
  ...(worktree !== undefined ? { worktree: worktree as boolean } : {}),
4270
4354
  ...(outputSchema !== undefined ? { outputSchema: outputSchema as JsonSchemaObject } : {}),
4271
4355
  ...(agentContract !== undefined ? { agentContract: agentContract as AgentContract } : {}),
@@ -4275,7 +4359,6 @@ export function prepareWorkflowLaunchParams(
4275
4359
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4276
4360
  ...(options.missionDetached ? { mission: false } : {}),
4277
4361
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
4278
- ...(turnBudget !== undefined ? { turnBudget: turnBudget as TurnBudgetConfig } : {}),
4279
4362
  ...(toolBudget !== undefined ? { toolBudget: toolBudget as ToolBudgetConfig } : {}),
4280
4363
  ...(intercomBridge !== undefined ? { intercomBridge: intercomBridge as IntercomBridgeConfig } : {}),
4281
4364
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
@@ -4289,6 +4372,9 @@ export function prepareWorkflowLaunchParams(
4289
4372
  ...(options.missionDetached ? { mission: false } : {}),
4290
4373
  workflowParentRunId: parentWorkflowRunId,
4291
4374
  workflowKey,
4375
+ ...(options.outputClaimPath ? { workflowOutputClaimPath: options.outputClaimPath } : {}),
4376
+ ...(options.awaitDetachedChild ? { workflowAwaitDetached: true } : {}),
4377
+ ...(lane ? { lane } : {}),
4292
4378
  ...(parentTimeoutMs !== undefined ? { workflowParentDeadlineAt: options.parentDeadlineAt } : {}),
4293
4379
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4294
4380
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
@@ -4330,11 +4416,12 @@ function workflowChatProgressUpdate(
4330
4416
  chatProgress: WorkflowChatProgressProjection,
4331
4417
  workflow: NonNullable<Details["workflow"]>,
4332
4418
  workflowChildren?: Details["workflowChildren"],
4419
+ preflight?: import("../../shared/types.ts").WorkflowPreflightV1,
4333
4420
  ): AgentToolResult<Details> | undefined {
4334
4421
  if (chatProgress.mode !== "live-card") return undefined;
4335
4422
  return {
4336
4423
  content: [{ type: "text", text: "Workflow running." }],
4337
- details: { mode: "workflow", runId, results: [], workflow, ...(workflowChildren ? { workflowChildren } : {}), chatProgress },
4424
+ details: { mode: "workflow", runId, results: [], workflow, ...(preflight ? { preflight } : {}), ...(workflowChildren ? { workflowChildren } : {}), chatProgress },
4338
4425
  };
4339
4426
  }
4340
4427
 
@@ -4423,14 +4510,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4423
4510
  preserveActiveSession = false,
4424
4511
  parentModelOverride?: ParentModel | null,
4425
4512
  ): Promise<AgentToolResult<Details>> => {
4426
- if (params.action?.trim() === "validate") {
4427
- const validation = validateWorkflowScript(params.workflowScript ?? "");
4428
- return {
4429
- content: [{ type: "text", text: JSON.stringify(validation) }],
4430
- ...(validation.ok ? {} : { isError: true }),
4431
- details: { mode: "management", results: [] },
4432
- };
4433
- }
4434
4513
  const workflowLaunchObserver = workflowLaunchObservers.get(params);
4435
4514
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
4436
4515
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
@@ -4444,7 +4523,36 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4444
4523
  deps.state.lastForegroundControlId ??= null;
4445
4524
  const normalizedGate = normalizeGateParams(params);
4446
4525
  if (!normalizedGate.ok) return buildRequestedModeError(params, normalizedGate.error);
4447
- const requestParams = normalizedGate.params;
4526
+ let requestParams = normalizedGate.params;
4527
+ let workflowPreflight: import("../../shared/types.ts").WorkflowPreflightV1 | undefined;
4528
+ try {
4529
+ if (requestParams.preflight !== undefined && requestParams.workflowScript === undefined && requestParams.workflowScriptPath === undefined) {
4530
+ throw new Error("preflight requires workflowScript or workflowScriptPath.");
4531
+ }
4532
+ workflowPreflight = normalizeWorkflowPreflight(requestParams.preflight);
4533
+ if (workflowPreflight) requestParams = { ...requestParams, preflight: workflowPreflight };
4534
+ } catch (error) {
4535
+ const message = error instanceof Error ? error.message : String(error);
4536
+ if (requestParams.action?.trim() === "validate") {
4537
+ const validation = validateWorkflowScript(requestParams.workflowScript ?? "");
4538
+ const invalidValidation = { ...validation, ok: false, errors: [...validation.errors, { message }] };
4539
+ return {
4540
+ content: [{ type: "text", text: JSON.stringify(invalidValidation) }],
4541
+ isError: true,
4542
+ details: { mode: "management", results: [] },
4543
+ };
4544
+ }
4545
+ return buildRequestedModeError(requestParams, message);
4546
+ }
4547
+ if (requestParams.action?.trim() === "validate") {
4548
+ const validation = validateWorkflowScript(requestParams.workflowScript ?? "");
4549
+ const payload = workflowPreflight ? { ...validation, preflight: workflowPreflight } : validation;
4550
+ return {
4551
+ content: [{ type: "text", text: JSON.stringify(payload) }],
4552
+ ...(validation.ok ? {} : { isError: true }),
4553
+ details: { mode: "management", results: [] },
4554
+ };
4555
+ }
4448
4556
  const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
4449
4557
  if (normalizedAction === "resume" && requestParams.extensionBindings !== undefined) return buildRequestedModeError(requestParams, "extensionBindings is not supported with action='resume'; resume uses the original retained child binding.");
4450
4558
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
@@ -4473,7 +4581,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4473
4581
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4474
4582
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4475
4583
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4476
- const workflowAgents = deps.discoverAgents(workflowCwd, resolveExecutionAgentScope(requestParams.agentScope)).agents;
4584
+ const discoverWorkflowAgents = (cwd: string, scope: AgentScope) => deps.discoverAgents(cwd, scope, workflowParentModel?.provider);
4585
+ const workflowAgents = discoverWorkflowAgents(workflowCwd, resolveExecutionAgentScope(requestParams.agentScope)).agents;
4477
4586
  const workflowArtifactConfig: ArtifactConfig = omitUndefinedProperties({
4478
4587
  ...DEFAULT_ARTIFACT_CONFIG,
4479
4588
  enabled: requestParams.artifacts !== false,
@@ -4602,6 +4711,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4602
4711
  ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}),
4603
4712
  pid: process.pid,
4604
4713
  steps: [],
4714
+ ...(workflowPreflight ? { preflight: workflowPreflight } : {}),
4605
4715
  workflow: { trace: [], emits: [], console: [] },
4606
4716
  workflowChildren: workflowChildSummary({ parentToolCallId: toolCallId, workflowRunId, workflowState: "running", inventoryComplete: false }),
4607
4717
  runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
@@ -4689,6 +4799,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4689
4799
  liveJob.turnCount = status.turnCount;
4690
4800
  liveJob.toolCount = status.toolCount;
4691
4801
  liveJob.currentStep = status.currentStep;
4802
+ liveJob.preflight = status.preflight;
4692
4803
  if (status.steps) {
4693
4804
  liveJob.steps = status.steps.map((step, index) => ({ ...step, index }));
4694
4805
  liveJob.agents = status.steps.map((step) => step.agent);
@@ -4732,7 +4843,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4732
4843
  status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
4733
4844
  status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
4734
4845
  };
4735
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}), status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow, workflowChildren: status.workflowChildren };
4846
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}), status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], ...(workflowPreflight ? { preflight: workflowPreflight } : {}), startedAt, updatedAt: startedAt, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow, workflowChildren: status.workflowChildren };
4736
4847
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
4737
4848
  deps.state.fleetJobs ??= new Map();
4738
4849
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
@@ -4752,18 +4863,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4752
4863
  const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
4753
4864
  const workflowResults: SingleResult[] = [];
4754
4865
  const workflowChildRunIds = new Map<string, string>();
4755
- const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
4866
+ const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, preflight: _preflight, ...workflowChildDefaults } = workflowRequest;
4756
4867
  const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
4757
4868
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4758
- const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId));
4869
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, parentCwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId));
4759
4870
  const claimedOutputPaths = new Map<string, string>();
4760
4871
  const childOutputOverrides = new Map<string, string>();
4872
+ const childOutputClaimPaths = new Map<string, string>();
4761
4873
  const producedChildOutputPaths = new Set<string>();
4762
4874
  const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4875
+ const runHostCommand = workflowHostCommandRunner({ workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, claimedOutputPaths, producedOutputPaths: producedChildOutputPaths });
4763
4876
  let projectedTraceLength = 0;
4764
4877
  let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
4765
4878
  const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
4766
- status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
4879
+ const projectedTrace = annotateWorkflowPreflightTrace(trace, workflowPreflight);
4880
+ const preflightWarnings = workflowPreflightWarnings(workflowPreflight, trace);
4881
+ status.workflow = {
4882
+ ...(status.workflow ?? { emits: [], console: [] }),
4883
+ trace: projectedTrace,
4884
+ ...(preflightWarnings.length ? { preflightWarnings } : {}),
4885
+ };
4767
4886
  const rebuild = trace.length < projectedTraceLength
4768
4887
  || (projectedTraceLength > 0 && trace[projectedTraceLength - 1] !== projectedTraceTail);
4769
4888
  if (rebuild) {
@@ -4776,8 +4895,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4776
4895
  for (let index = projectedTraceLength; index < trace.length; index += 1) {
4777
4896
  const entry = trace[index]!;
4778
4897
  if (entry.operation !== "run") continue;
4898
+ const entryLabel = entry.label?.trim() || undefined;
4899
+ const entryPhase = entry.phase?.trim() || undefined;
4900
+ const shouldProjectPhase = entryPhase !== undefined && (entryPhase !== "auto-resume" || workflowSteps.get(entry.key)?.phase === undefined);
4779
4901
  const existing = workflowSteps.get(entry.key);
4780
- if (entry.state === "reused" && existing) continue;
4902
+ if (existing) {
4903
+ if (entryLabel) existing.label = entryLabel;
4904
+ if (shouldProjectPhase) existing.phase = entryPhase;
4905
+ }
4906
+ if (entry.state === "reused" && existing) {
4907
+ continue;
4908
+ }
4781
4909
  const mapped = entry.state === "started" || entry.state === "reused"
4782
4910
  ? "running"
4783
4911
  : entry.state === "completed"
@@ -4801,13 +4929,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4801
4929
  if (entry.durationMs === undefined) delete existing.durationMs;
4802
4930
  else existing.durationMs = entry.durationMs;
4803
4931
  } else {
4932
+ // Naming uses the explicit label only — never the workflow key — so the
4933
+ // placeholder cannot diverge from the task-derived name the child
4934
+ // session actually gets (launch/progress updates overwrite this).
4935
+ const stepSessionName = deriveChildSessionName({ agent: entry.agent ?? entry.key, label: entryLabel });
4804
4936
  const step: NonNullable<AsyncStatus["steps"]>[number] = {
4805
4937
  agent: entry.agent ?? entry.key,
4806
- label: entry.key,
4938
+ ...(stepSessionName ? { sessionName: stepSessionName } : {}),
4939
+ label: entryLabel ?? entry.key,
4807
4940
  workflowKey: entry.key,
4808
4941
  parentWorkflowRunId: workflowRunId,
4809
4942
  status: mapped,
4810
4943
  startedAt: Date.now(),
4944
+ ...(entryPhase ? { phase: entryPhase } : {}),
4811
4945
  ...(entry.runId ? { runId: entry.runId } : {}),
4812
4946
  ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
4813
4947
  ...(entry.state === "detached" ? { activityState: "needs_attention" as const } : {}),
@@ -4843,6 +4977,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4843
4977
  try {
4844
4978
  const workflow = await runWorkflowScript({
4845
4979
  script: workflowScript,
4980
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
4846
4981
  timeoutMs: timeout,
4847
4982
  signal: controller.signal,
4848
4983
  registerStopChild: (stop) => {
@@ -4851,11 +4986,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4851
4986
  },
4852
4987
  ...(workflowState ? { state: workflowState } : {}),
4853
4988
  onTrace: updateTrace,
4989
+ host: runHostCommand,
4990
+ onHostStep: (hostStep) => {
4991
+ status = upsertHostStep({ status, hostStep, persist: (nextStatus) => {
4992
+ status = nextStatus;
4993
+ persist({ tolerateStatusWriteFailure: true });
4994
+ } });
4995
+ },
4854
4996
  admit: (calls) => {
4855
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4997
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: parentCwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4856
4998
  if (outputClaims.error) throw new Error(outputClaims.error);
4857
4999
  status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4858
5000
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
5001
+ if (outputClaims.childClaims) for (const [key, claimPath] of outputClaims.childClaims) childOutputClaimPaths.set(key, claimPath);
4859
5002
  if (outputClaims.overrides) for (const [key, output] of outputClaims.overrides) childOutputOverrides.set(key, output);
4860
5003
  persist({ tolerateStatusWriteFailure: true });
4861
5004
  },
@@ -4878,19 +5021,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4878
5021
  ...(childPhase ? { phase: childPhase } : {}),
4879
5022
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4880
5023
  });
5024
+ let preparedChildParams: SubagentParamsLike | undefined;
4881
5025
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4882
5026
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4883
- { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
5027
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: parentCwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), outputClaimPath: childOutputClaimPaths.get(key), options: { missionDetached: detachWorkflowChildMissions, awaitDetachedChild: true, runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
4884
5028
  missionBinding,
4885
5029
  deps.asyncByDefault,
4886
5030
  );
5031
+ preparedChildParams = childRequest;
4887
5032
  workflowLaunchObservers.set(childRequest, (launch) => {
4888
5033
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4889
5034
  if (step) {
4890
5035
  step.agent = launch.agent;
5036
+ if (launch.sessionName) step.sessionName = launch.sessionName;
4891
5037
  step.sessionFile = launch.sessionFile;
4892
5038
  step.async = launch.async;
4893
5039
  if (launch.runId) step.runId = launch.runId;
5040
+ if (childRequest.lane) step.lane = childRequest.lane;
4894
5041
  persist({ tolerateStatusWriteFailure: true });
4895
5042
  }
4896
5043
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
@@ -4912,6 +5059,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4912
5059
  step.toolCount = progress.toolCount;
4913
5060
  step.model = progress.model;
4914
5061
  step.thinking = progress.thinking;
5062
+ if (progress.sessionName) step.sessionName = progress.sessionName;
4915
5063
  step.error = progress.error;
4916
5064
  projectWorkflowActivity();
4917
5065
  persist({ tolerateStatusWriteFailure: true });
@@ -4923,14 +5071,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4923
5071
  });
4924
5072
  workflowResults.push(...result.details.results);
4925
5073
  for (const childResult of result.details.results) {
4926
- if (childResult.savedOutputPath) producedChildOutputPaths.add(childResult.savedOutputPath);
5074
+ if (childResult.savedOutputPath) producedChildOutputPaths.add(resolveWorkflowHostOutputClaimPath(childResult.savedOutputPath));
4927
5075
  }
4928
- const child = workflowChildResult(key, result, childParams, deps.state);
5076
+ const child = workflowChildResult(key, result, preparedChildParams ?? childParams, deps.state);
4929
5077
  if (child.runId) workflowChildRunIds.set(key, child.runId);
4930
5078
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4931
5079
  if (step) {
4932
5080
  step.async = Boolean(result.details.asyncId || result.details.asyncDir);
4933
5081
  if (child.runId) step.runId = child.runId;
5082
+ if (child.lane) step.lane = child.lane;
4934
5083
  }
4935
5084
  if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4936
5085
  const childStatus = missionWorkflowChildStatus(result);
@@ -4953,22 +5102,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4953
5102
  resolveResume: (reference) => resolveKeyedWorkflowResume(reference, deps.state),
4954
5103
  steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
4955
5104
  });
5105
+ const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, workflow.trace, { settled: true });
5106
+ const finalPreflightTrace = annotateWorkflowPreflightTrace(workflow.trace, workflowPreflight);
4956
5107
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4957
5108
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4958
- const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).${workflowOutputPathMappingSummary(workflow.children)}`;
5109
+ const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).${workflowOutputPathMappingSummary(workflow.children)}${finalPreflightWarnings.length ? ` ${finalPreflightWarnings.join(" ")}` : ""}`;
4959
5110
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary, producedChildOutputPaths);
4960
5111
  const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
4961
5112
  const workflowUsage = sumResultsUsage(workflowResults);
4962
5113
  const workflowChildren = workflowChildSummary({ parentToolCallId: toolCallId, workflowRunId, workflowState: "completed", inventoryComplete: true, trace: workflow.trace, children: workflow.children, steps: status.steps });
4963
- status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, workflowChildren, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4964
- const receipt = terminalWorkflowReceipt(workflowRunId, "complete", workflow.children, workflowChildren);
5114
+ status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}) }, workflowChildren, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
5115
+ const receipt = terminalWorkflowReceipt(workflowRunId, "complete", workflow.children, workflowChildren, undefined, validHostStepNodes(status.workflowGraph));
4965
5116
  let workflowReceipt: { path: string; receipt: WorkflowReceipt } | undefined;
4966
5117
  try {
4967
5118
  workflowReceipt = { path: writeWorkflowReceipt(asyncDir, receipt), receipt };
4968
5119
  } catch (receiptError) {
4969
5120
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
4970
5121
  }
4971
- if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, workflowChildren, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5122
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, workflowChildren, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), ...(status.steps?.find((step) => step.workflowKey === child.key)?.sessionName ? { sessionName: status.steps?.find((step) => step.workflowKey === child.key)?.sessionName } : {}), ...workflowChildAccountingFields(child), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.outputReference ? { outputReference: child.outputReference } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4972
5123
  persist();
4973
5124
  persistClosed = true;
4974
5125
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
@@ -4992,26 +5143,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4992
5143
  }
4993
5144
  const workflowState = state === "paused" ? "paused" : state === "stopped" ? "stopped" : "failed";
4994
5145
  const workflowChildren = workflowChildSummary({ parentToolCallId: toolCallId, workflowRunId, workflowState, inventoryComplete: true, trace: partial.trace, children: partial.children, steps: status.steps });
4995
- status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: workflowFailureMessage(error, workflowRunId, partial.children), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console }, workflowChildren });
5146
+ const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, partial.trace, { settled: true });
5147
+ const finalPreflightTrace = annotateWorkflowPreflightTrace(partial.trace, workflowPreflight);
5148
+ status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: workflowFailureMessage(error, workflowRunId, partial.children), endedAt: Date.now(), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}) }, workflowChildren });
4996
5149
  if (pauseForDetached) {
4997
5150
  const promoted = promotePausedWorkflowIfSettled(status);
4998
5151
  if (promoted) status = promoted;
4999
5152
  }
5000
5153
  const terminalSummary = `${status.state === "complete"
5001
5154
  ? "Workflow completed after detached child finished."
5002
- : status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.")}${workflowOutputPathMappingSummary(partial.children)}`;
5155
+ : status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.")}${workflowOutputPathMappingSummary(partial.children)}${finalPreflightWarnings.length ? ` ${finalPreflightWarnings.join(" ")}` : ""}`;
5003
5156
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, terminalSummary, producedChildOutputPaths);
5004
5157
  const resultSummary = appendWorkflowOutputWarning(terminalSummary, outputWarning);
5005
5158
  const receiptState: WorkflowReceiptState = status.state === "complete" ? "complete" : status.state === "paused" ? "paused" : status.state === "stopped" ? "stopped" : "failed";
5006
5159
  const terminalOutcome = workflowFailureTerminalOutcome(error, partial.children, usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)));
5007
- const receipt = terminalWorkflowReceipt(workflowRunId, receiptState, partial.children, workflowChildren, terminalOutcome);
5160
+ const receipt = terminalWorkflowReceipt(workflowRunId, receiptState, partial.children, workflowChildren, terminalOutcome, validHostStepNodes(status.workflowGraph));
5008
5161
  let workflowReceipt: { path: string; receipt: WorkflowReceipt } | undefined;
5009
5162
  try {
5010
5163
  workflowReceipt = { path: writeWorkflowReceipt(asyncDir, receipt), receipt };
5011
5164
  } catch (receiptError) {
5012
5165
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
5013
5166
  }
5014
- if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, workflowChildren, ...(terminalOutcome ? { terminalOutcome } : {}), results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.terminalOutcome ? { terminalOutcome: child.terminalOutcome } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5167
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, workflowChildren, ...(terminalOutcome ? { terminalOutcome } : {}), results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), ...(status.steps?.find((step) => step.workflowKey === child.key)?.sessionName ? { sessionName: status.steps?.find((step) => step.workflowKey === child.key)?.sessionName } : {}), ...workflowChildAccountingFields(child), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.outputReference ? { outputReference: child.outputReference } : {}), ...(child.terminalOutcome ? { terminalOutcome: child.terminalOutcome } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5015
5168
  persist();
5016
5169
  persistClosed = true;
5017
5170
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(terminalOutcome ? { terminalOutcome } : {}), ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
@@ -5024,44 +5177,55 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5024
5177
  }
5025
5178
  });
5026
5179
  return attachWorkflowMission(withRunFanoutBudget({
5027
- content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
5028
- details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], workflowChildren: status.workflowChildren, chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
5180
+ content: [{ type: "text", text: `${workflowPreflight ? `${formatWorkflowPreflight(workflowPreflight)}\n\n` : ""}${formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true)}` }],
5181
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren: status.workflowChildren, chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
5029
5182
  }, workflowFanoutBudget));
5030
5183
  }
5031
- const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
5184
+ const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, preflight: _preflight, ...workflowChildDefaults } = requestParams;
5032
5185
  const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
5033
5186
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5034
5187
  const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id));
5035
5188
  const claimedOutputPaths = new Map<string, string>();
5036
5189
  const childOutputOverrides = new Map<string, string>();
5190
+ const childOutputClaimPaths = new Map<string, string>();
5037
5191
  const producedChildOutputPaths = new Set<string>();
5038
5192
  const workflowResults: SingleResult[] = [];
5039
5193
  const workflowChildRunIds = new Map<string, string>();
5194
+ const runHostCommand = workflowHostCommandRunner({ workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, claimedOutputPaths, producedOutputPaths: producedChildOutputPaths });
5195
+ const workflowHostSteps = new Map<string, HostStepNodeV1>();
5040
5196
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
5041
5197
  let liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "running", inventoryComplete: false });
5042
5198
  const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
5043
5199
  const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(resolveCurrentSessionId(ctx.sessionManager)));
5044
5200
  const sendWorkflowProgress = () => {
5045
- const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow, liveWorkflowChildren);
5201
+ const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow, liveWorkflowChildren, workflowPreflight);
5046
5202
  if (update) onUpdate?.(update);
5047
5203
  };
5048
5204
  try {
5049
5205
  const workflow = await runWorkflowScript({
5050
5206
  script: requestParams.workflowScript,
5051
5207
  ...(delegatedWorkflowPermit ? { oneUsePermit: { claim: (key: string) => claimWorkflowChildPermit(delegatedWorkflowPermit, _id, key) } } : {}),
5208
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
5052
5209
  timeoutMs: timeout,
5053
5210
  signal,
5054
5211
  ...(workflowState ? { state: workflowState } : {}),
5212
+ ...(!delegatedWorkflowPermit ? { host: runHostCommand } : {}),
5213
+ onHostStep: (hostStep) => {
5214
+ workflowHostSteps.set(hostStep.id, { ...hostStep, ...(hostStep.freshness ? { freshness: { ...hostStep.freshness } } : {}) });
5215
+ },
5055
5216
  onTrace: (trace) => {
5056
- liveWorkflow = { ...liveWorkflow, trace };
5217
+ const projectedTrace = annotateWorkflowPreflightTrace(trace, workflowPreflight);
5218
+ const preflightWarnings = workflowPreflightWarnings(workflowPreflight, trace);
5219
+ liveWorkflow = { ...liveWorkflow, trace: projectedTrace, ...(preflightWarnings.length ? { preflightWarnings } : {}) };
5057
5220
  liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "running", inventoryComplete: false, trace });
5058
5221
  sendWorkflowProgress();
5059
5222
  },
5060
5223
  admit: (calls) => {
5061
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5224
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5062
5225
  if (outputClaims.error) throw new Error(outputClaims.error);
5063
5226
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5064
5227
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
5228
+ if (outputClaims.childClaims) for (const [key, claimPath] of outputClaims.childClaims) childOutputClaimPaths.set(key, claimPath);
5065
5229
  if (outputClaims.overrides) for (const [key, output] of outputClaims.overrides) childOutputOverrides.set(key, output);
5066
5230
  },
5067
5231
  onEmit: (emits) => {
@@ -5084,17 +5248,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5084
5248
  ...(childPhase ? { phase: childPhase } : {}),
5085
5249
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
5086
5250
  });
5251
+ let preparedChildParams: SubagentParamsLike | undefined;
5087
5252
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
5088
5253
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5089
- { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
5254
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), outputClaimPath: childOutputClaimPaths.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
5090
5255
  missionBinding,
5091
5256
  deps.asyncByDefault,
5092
5257
  );
5258
+ preparedChildParams = childRequest;
5093
5259
  if (delegatedWorkflowPermit) {
5094
5260
  if (childRequest.async !== false) throw new Error("Workflow child permit supports one foreground child only.");
5095
5261
  const childCwd = resolveRequestedCwd(workflowCwd, childRequest.cwd);
5096
5262
  const childAgent = typeof childRequest.agent === "string"
5097
- ? resolveAgentName(childRequest.agent, deps.discoverAgents(childCwd, resolveExecutionAgentScope(childRequest.agentScope)).agents).agent
5263
+ ? resolveAgentName(childRequest.agent, discoverWorkflowAgents(childCwd, resolveExecutionAgentScope(childRequest.agentScope)).agents).agent
5098
5264
  : undefined;
5099
5265
  if (childAgent?.runner?.type === "external-cli" || childAgent?.runner?.type === "external-job") throw new Error("Workflow child permit supports native Pi children only.");
5100
5266
  workflowPermitContexts.set(childRequest, { child: { permit: delegatedWorkflowPermit, workflowRunId: _id, childKey: key } });
@@ -5116,10 +5282,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5116
5282
  });
5117
5283
  workflowResults.push(...result.details.results);
5118
5284
  for (const childResult of result.details.results) {
5119
- if (childResult.savedOutputPath) producedChildOutputPaths.add(childResult.savedOutputPath);
5285
+ if (childResult.savedOutputPath) producedChildOutputPaths.add(resolveWorkflowHostOutputClaimPath(childResult.savedOutputPath));
5120
5286
  }
5121
5287
  if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
5122
- const child = workflowChildResult(key, result, childParams, deps.state);
5288
+ const child = workflowChildResult(key, result, preparedChildParams ?? childParams, deps.state);
5123
5289
  if (child.runId) workflowChildRunIds.set(key, child.runId);
5124
5290
  const childStatus = missionWorkflowChildStatus(result);
5125
5291
  recordMissionWorkflowChild(missionBinding, _id, key, {
@@ -5137,13 +5303,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5137
5303
  resolveResume: (reference) => resolveKeyedWorkflowResume(reference, deps.state),
5138
5304
  steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId: _id, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
5139
5305
  });
5306
+ const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, workflow.trace, { settled: true });
5307
+ const finalPreflightTrace = annotateWorkflowPreflightTrace(workflow.trace, workflowPreflight);
5140
5308
  const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "completed", inventoryComplete: true, trace: workflow.trace, children: workflow.children });
5141
- const receipt = terminalWorkflowReceipt(_id, "complete", workflow.children, workflowChildren);
5142
- const traceLines = workflow.trace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.durationMs !== undefined ? ` in ${entry.durationMs}ms` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
5143
- const sections = ["Workflow completed.", `Return:\n${formatWorkflowValue(workflow.value)}`];
5309
+ const receipt = terminalWorkflowReceipt(_id, "complete", workflow.children, workflowChildren, undefined, [...workflowHostSteps.values()]);
5310
+ const traceLines = finalPreflightTrace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.durationMs !== undefined ? ` in ${entry.durationMs}ms` : ""}${entry.warning ? ` — ${entry.warning}` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
5311
+ const sections = [
5312
+ ...(workflowPreflight ? [formatWorkflowPreflight(workflowPreflight)] : []),
5313
+ "Workflow completed.",
5314
+ `Return:\n${formatWorkflowValue(workflow.value)}`,
5315
+ ];
5144
5316
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
5145
5317
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5146
5318
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5319
+ if (finalPreflightWarnings.length > 0) sections.push(formatWorkflowPreflightWarnings(finalPreflightWarnings));
5147
5320
  const outputMappings = workflowOutputPathMappingSummary(workflow.children).trim();
5148
5321
  if (outputMappings) sections.push(outputMappings);
5149
5322
  const workflowText = sections.join("\n\n");
@@ -5151,16 +5324,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5151
5324
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5152
5325
  return attachWorkflowMission(withRunFanoutBudget({
5153
5326
  content: [{ type: "text", text: displayText }],
5154
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console, receipt }, chatProgress }),
5327
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5155
5328
  }, workflowFanoutBudget));
5156
5329
  } catch (error) {
5157
5330
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
5158
5331
  const text = workflowFailureMessage(error, _id, partial.children);
5159
- const traceLines = partial.trace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
5160
- const sections = [`Workflow failed: ${text}`];
5332
+ const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, partial.trace, { settled: true });
5333
+ const finalPreflightTrace = annotateWorkflowPreflightTrace(partial.trace, workflowPreflight);
5334
+ const traceLines = finalPreflightTrace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.warning ? ` — ${entry.warning}` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
5335
+ const sections = [
5336
+ ...(workflowPreflight ? [formatWorkflowPreflight(workflowPreflight)] : []),
5337
+ `Workflow failed: ${text}`,
5338
+ ];
5161
5339
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
5162
5340
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5163
5341
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5342
+ if (finalPreflightWarnings.length > 0) sections.push(formatWorkflowPreflightWarnings(finalPreflightWarnings));
5164
5343
  const outputMappings = workflowOutputPathMappingSummary(partial.children).trim();
5165
5344
  if (outputMappings) sections.push(outputMappings);
5166
5345
  const workflowText = sections.join("\n\n");
@@ -5168,11 +5347,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5168
5347
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5169
5348
  const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "failed", inventoryComplete: true, trace: partial.trace, children: partial.children });
5170
5349
  const terminalOutcome = workflowFailureTerminalOutcome(error, partial.children, usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)));
5171
- const receipt = terminalWorkflowReceipt(_id, "failed", partial.children, workflowChildren, terminalOutcome);
5350
+ const receipt = terminalWorkflowReceipt(_id, "failed", partial.children, workflowChildren, terminalOutcome, [...workflowHostSteps.values()]);
5172
5351
  return attachWorkflowMission(withRunFanoutBudget({
5173
5352
  content: [{ type: "text", text: displayText }],
5174
5353
  isError: true,
5175
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console, receipt }, chatProgress }),
5354
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5176
5355
  }, workflowFanoutBudget));
5177
5356
  }
5178
5357
  }
@@ -5197,6 +5376,35 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5197
5376
  requestParentModel = normalizeParentModel(ctx.model);
5198
5377
  }
5199
5378
  if (action) {
5379
+ if (action === "worktree.cleanup") {
5380
+ if (deps.allowMutatingManagementActions === false) {
5381
+ return { content: [{ type: "text", text: "Action 'worktree.cleanup' is not available from child-safe subagent fanout mode." }], isError: true, details: { mode: "management", results: [] } };
5382
+ }
5383
+ if (paramsWithResolvedCwd.mode !== "plan") {
5384
+ return { content: [{ type: "text", text: "worktree.cleanup currently supports mode='plan' only; apply/removal is not available yet." }], isError: true, details: { mode: "management", results: [] } };
5385
+ }
5386
+ if (paramsWithResolvedCwd.planId !== undefined) {
5387
+ return { content: [{ type: "text", text: "worktree.cleanup plan mode does not accept planId; apply is not available yet." }], isError: true, details: { mode: "management", results: [] } };
5388
+ }
5389
+ try {
5390
+ const created = createWorktreeCleanupPlan({
5391
+ repo: paramsWithResolvedCwd.repo?.trim()
5392
+ ? path.isAbsolute(paramsWithResolvedCwd.repo) ? paramsWithResolvedCwd.repo : path.resolve(requestCwd, paramsWithResolvedCwd.repo)
5393
+ : requestCwd,
5394
+ ...(paramsWithResolvedCwd.handoffPath ? { handoffPath: path.isAbsolute(paramsWithResolvedCwd.handoffPath) ? paramsWithResolvedCwd.handoffPath : path.resolve(requestCwd, paramsWithResolvedCwd.handoffPath) } : {}),
5395
+ ...(deps.config.worktreeBaseDir ? { worktreeBaseDir: deps.config.worktreeBaseDir } : {}),
5396
+ foregroundRunOwnership: (runId: string) => {
5397
+ if (deps.state.foregroundControls.has(runId)) return "active" as const;
5398
+ const remembered = deps.state.foregroundRuns?.get(runId);
5399
+ if (!remembered || remembered.children.length === 0 || remembered.children.some((child) => child.status === "detached")) return "unknown" as const;
5400
+ return "terminal" as const;
5401
+ },
5402
+ });
5403
+ return { content: [{ type: "text", text: formatWorktreeCleanupPlan(created) }], details: { mode: "management", results: [] } };
5404
+ } catch (error) {
5405
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "management", results: [] } };
5406
+ }
5407
+ }
5200
5408
  if (action === "worktree.discard") {
5201
5409
  if (deps.allowMutatingManagementActions === false) {
5202
5410
  return { content: [{ type: "text", text: "Action 'worktree.discard' is not available from child-safe subagent fanout mode." }], isError: true, details: { mode: "management", results: [] } };
@@ -5224,6 +5432,30 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5224
5432
  return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "management", results: [] } };
5225
5433
  }
5226
5434
  }
5435
+ if (action === "lane.status" || action === "lane.recordMerge" || action === "lane.recordSupersession") {
5436
+ if (action !== "lane.status" && deps.allowMutatingManagementActions === false) {
5437
+ return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
5438
+ }
5439
+ const laneId = paramsWithResolvedCwd.laneId?.trim();
5440
+ if (!laneId) return { content: [{ type: "text", text: `${action} requires laneId.` }], isError: true, details: { mode: "management", results: [] } };
5441
+ const handoffPath = paramsWithResolvedCwd.handoffPath?.trim();
5442
+ if (!handoffPath) return { content: [{ type: "text", text: `${action} requires handoffPath for the existing parallel handoff manifest.` }], isError: true, details: { mode: "management", results: [] } };
5443
+ const manifestPath = path.isAbsolute(handoffPath) ? handoffPath : path.resolve(requestCwd, handoffPath);
5444
+ try {
5445
+ if (action === "lane.status") {
5446
+ let manifest;
5447
+ try { manifest = readParallelHandoffManifest(manifestPath); } catch { manifest = undefined; }
5448
+ if (manifest && manifest.runId !== laneId) throw new Error(`Lane '${laneId}' does not match manifest run '${manifest.runId}'.`);
5449
+ return { content: [{ type: "text", text: formatStoredParallelHandoffCleanup(manifestPath, manifest) }], details: { mode: "management", results: [] } };
5450
+ }
5451
+ const recorded = action === "lane.recordMerge"
5452
+ ? recordParallelHandoffMerge({ manifestPath, laneId, merge: paramsWithResolvedCwd.merge })
5453
+ : recordParallelHandoffSupersession({ manifestPath, laneId, supersession: paramsWithResolvedCwd.supersession });
5454
+ return { content: [{ type: "text", text: recorded.text }], details: { mode: "management", results: [], parallelHandoff: recorded.reference } };
5455
+ } catch (error) {
5456
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "management", results: [] } };
5457
+ }
5458
+ }
5227
5459
  if ((HERDR_PROJECT_PANE_ACTIONS as readonly string[]).includes(action)) {
5228
5460
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
5229
5461
  return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
@@ -5501,6 +5733,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5501
5733
  return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel, signal }));
5502
5734
  }
5503
5735
  if (action === "steer") {
5736
+ if (paramsWithResolvedCwd.mode !== undefined && resolveSteerDeliveryMode(paramsWithResolvedCwd.mode) === undefined) {
5737
+ return { content: [{ type: "text", text: "action='steer' mode must be 'steer', 'follow_up', or 'auto'." }], isError: true, details: { mode: "management", results: [] } };
5738
+ }
5504
5739
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
5505
5740
  const message = (paramsWithResolvedCwd.message ?? paramsWithResolvedCwd.task ?? "").trim();
5506
5741
  if (!message) return { content: [{ type: "text", text: "action='steer' requires message." }], isError: true, details: { mode: "management", results: [] } };
@@ -5513,7 +5748,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5513
5748
  if (directoryStatus?.mode === "workflow") {
5514
5749
  const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: directoryStatus.runId || runId, asyncDirRoot: DIRS.async });
5515
5750
  if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5516
- return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5751
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: resolveSteerDeliveryMode(paramsWithResolvedCwd.mode), index: paramsWithResolvedCwd.index, signal });
5517
5752
  }
5518
5753
  if (location.asyncDir) {
5519
5754
  const unsupported = externalRunnerControlError(location.asyncDir, "steer");
@@ -5523,7 +5758,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5523
5758
  state: deps.state,
5524
5759
  runId,
5525
5760
  message,
5526
- mode: paramsWithResolvedCwd.mode,
5761
+ mode: resolveSteerDeliveryMode(paramsWithResolvedCwd.mode),
5527
5762
  index: paramsWithResolvedCwd.index,
5528
5763
  kill: deps.kill,
5529
5764
  location,
@@ -5549,18 +5784,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5549
5784
  const text = error instanceof Error ? error.message : String(error);
5550
5785
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
5551
5786
  }
5552
- if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal }));
5787
+ if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, mode: resolveSteerDeliveryMode(paramsWithResolvedCwd.mode), index: paramsWithResolvedCwd.index, signal }));
5553
5788
  if (resolved?.kind === "foreground") {
5554
5789
  const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, childRunId: resolved.id, asyncDirRoot: DIRS.async });
5555
5790
  if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5556
- return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5791
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: resolveSteerDeliveryMode(paramsWithResolvedCwd.mode), index: paramsWithResolvedCwd.index, signal });
5557
5792
  }
5558
5793
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
5559
5794
  const resolvedStatus = resolved.location.asyncDir ? readStatus(resolved.location.asyncDir) : null;
5560
5795
  if (resolvedStatus?.mode === "workflow") {
5561
5796
  const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: resolvedStatus.runId || resolved.id, asyncDirRoot: DIRS.async });
5562
5797
  if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5563
- return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5798
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: resolveSteerDeliveryMode(paramsWithResolvedCwd.mode), index: paramsWithResolvedCwd.index, signal });
5564
5799
  }
5565
5800
  if (resolved.location.asyncDir) {
5566
5801
  const unsupported = externalRunnerControlError(resolved.location.asyncDir, "steer");
@@ -5570,7 +5805,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5570
5805
  state: deps.state,
5571
5806
  runId: resolved.id,
5572
5807
  message,
5573
- mode: paramsWithResolvedCwd.mode,
5808
+ mode: resolveSteerDeliveryMode(paramsWithResolvedCwd.mode),
5574
5809
  index: paramsWithResolvedCwd.index,
5575
5810
  kill: deps.kill,
5576
5811
  location: resolved.location,
@@ -5809,7 +6044,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5809
6044
  const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
5810
6045
  const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
5811
6046
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
5812
- const discovered = deps.discoverAgents(effectiveCwd, scope);
6047
+ const discovered = deps.discoverAgents(effectiveCwd, scope, requestParentModel?.provider);
5813
6048
  const discoveredAgents = discovered.agents;
5814
6049
  const unknownAgentDiagnosticContext = diagnosticContextFromDiscovery(discovered, effectiveCwd, scope);
5815
6050
  const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics, unknownAgentDiagnosticContext);
@@ -5817,8 +6052,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5817
6052
  effectiveParams = canonicalParams.params!;
5818
6053
  const modelScope = discovered.modelScope;
5819
6054
  effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
5820
- const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget);
5821
- if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
5822
6055
  // An agent-level defaultContext is a preference, unlike an explicit request.
5823
6056
  // Prefer fork only when the parent session is persisted and has a current leaf;
5824
6057
  // otherwise use fresh immediately instead of launching a guaranteed-to-fail fork.
@@ -6138,7 +6371,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6138
6371
  intercomBridge,
6139
6372
  nestedRoute,
6140
6373
  timeoutMs: foregroundTimeout.timeoutMs,
6141
- turnBudget: turnBudget.turnBudget,
6142
6374
  toolBudget: runToolBudget.toolBudget,
6143
6375
  usageBudget: usageBudget.budget,
6144
6376
  allowZeroToolBudget,
@@ -6283,6 +6515,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6283
6515
  mode: foregroundMode,
6284
6516
  state,
6285
6517
  agent: agentsForSummary[0],
6518
+ ...(details?.results.length === 1 && details.results[0]?.sessionName ? { sessionName: details.results[0].sessionName } : {}),
6286
6519
  agents: agentsForSummary,
6287
6520
  ...(agentsForSummary.length === 1 && (type === "subagent.nested.started" ? startedLaunches[0]?.model : details?.results[0]?.model) ? { model: type === "subagent.nested.started" ? startedLaunches[0]?.model : details?.results[0]?.model } : {}),
6288
6521
  ...(agentsForSummary.length === 1 && (type === "subagent.nested.started" ? startedLaunches[0]?.thinking : details?.results[0]?.thinking) ? { thinking: type === "subagent.nested.started" ? startedLaunches[0]?.thinking : details?.results[0]?.thinking } : {}),
@@ -6301,6 +6534,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6301
6534
  : details?.results.length
6302
6535
  ? { steps: details.results.map((child) => ({
6303
6536
  agent: child.agent,
6537
+ ...(child.sessionName ? { sessionName: child.sessionName } : {}),
6304
6538
  status: child.interrupted || child.detached ? "paused" as const : child.exitCode === 0 ? "complete" as const : "failed" as const,
6305
6539
  ...(child.model ? { model: child.model } : {}),
6306
6540
  ...(child.thinking ? { thinking: child.thinking } : {}),
@@ -6324,10 +6558,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6324
6558
  }
6325
6559
  if (workflowLaunchObserver) {
6326
6560
  const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
6561
+ const singleSessionName = hasSingle
6562
+ ? deriveChildSessionName({ agent: effectiveParams.agent!, task: effectiveParams.task })
6563
+ : singleTask
6564
+ ? deriveChildSessionName({ agent: singleTask.agent, task: singleTask.task })
6565
+ : undefined;
6327
6566
  const launch = hasSingle
6328
- ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
6567
+ ? { agent: effectiveParams.agent!, ...(singleSessionName ? { sessionName: singleSessionName } : {}), sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model), async: effectiveAsync, runId: effectiveAsync ? asyncRunId : runId }
6329
6568
  : singleTask
6330
- ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
6569
+ ? { agent: singleTask.agent, ...(singleSessionName ? { sessionName: singleSessionName } : {}), sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model), async: effectiveAsync, runId: effectiveAsync ? asyncRunId : runId }
6331
6570
  : undefined;
6332
6571
  if (launch) {
6333
6572
  workflowLaunchObservers.delete(params);
@@ -6379,15 +6618,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6379
6618
  ): Promise<AgentToolResult<Details>> => {
6380
6619
  const normalizedAction = typeof params.action === "string" ? params.action.trim() : params.action;
6381
6620
  const requestParams = normalizedAction ? { ...params, action: normalizedAction } : params;
6382
- if (normalizedAction) return execute(id, requestParams, signal, onUpdate, ctx);
6621
+ if (normalizedAction) return execute(id, requestParams, signal, onUpdate, ctx).then(withAggregatedToolUsage);
6383
6622
  const { depth } = checkSubagentDepth(deps.config.maxSubagentDepth);
6384
6623
  const dispatchParams = applyForceTopLevelAsyncOverride(requestParams, depth, deps.config.forceTopLevelAsync === true);
6385
6624
  const runsForeground = dispatchParams.clarify === true || (dispatchParams.async ?? deps.asyncByDefault) !== true;
6386
- if (!runsForeground) return execute(id, requestParams, signal, onUpdate, ctx);
6625
+ if (!runsForeground) return execute(id, requestParams, signal, onUpdate, ctx).then(withAggregatedToolUsage);
6387
6626
  if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(requestParams);
6388
6627
  deps.state.subagentInProgress = true;
6389
6628
  try {
6390
- return await execute(id, requestParams, signal, onUpdate, ctx);
6629
+ return withAggregatedToolUsage(await execute(id, requestParams, signal, onUpdate, ctx));
6391
6630
  } finally {
6392
6631
  deps.state.subagentInProgress = false;
6393
6632
  }
@@ -6400,7 +6639,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6400
6639
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
6401
6640
  ctx: ExtensionContext,
6402
6641
  ): Promise<AgentToolResult<Details>> => {
6403
- const normalized = normalizePublicSubagentExecution(params, { asyncByDefault: deps.asyncByDefault });
6642
+ const normalized = normalizePublicSubagentExecution(params);
6404
6643
  if (!normalized.ok) {
6405
6644
  return Promise.resolve({ content: [{ type: "text", text: normalized.error }], isError: true, details: { mode: normalized.mode, results: [] } });
6406
6645
  }
@@ -6434,7 +6673,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6434
6673
  if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
6435
6674
  if (workflowPermit) workflowPermitContexts.set(delegatedParams, { root: workflowPermit });
6436
6675
  delegatedExecutions.add(delegatedParams);
6437
- return execute(id, delegatedParams, signal, onUpdate, ctx);
6676
+ return withAggregatedToolUsage(await execute(id, delegatedParams, signal, onUpdate, ctx));
6438
6677
  };
6439
6678
 
6440
6679
  const executeScheduled = (