pi-subagents 0.42.1 → 0.44.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 (59) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -5
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +2 -2
  5. package/skills/pi-subagents/references/constraints-and-recipes.md +35 -32
  6. package/skills/pi-subagents/references/execution-controls.md +30 -44
  7. package/skills/pi-subagents/references/management-authoring-rpc.md +20 -3
  8. package/skills/pi-subagents/references/prompting-and-roles.md +16 -49
  9. package/src/agents/agent-refinements.ts +624 -0
  10. package/src/agents/agents.ts +0 -2
  11. package/src/agents/proactive-skills.ts +1 -1
  12. package/src/api/delegation.ts +1 -2
  13. package/src/extension/config.ts +28 -7
  14. package/src/extension/control-notices.ts +2 -2
  15. package/src/extension/fanout-child.ts +46 -22
  16. package/src/extension/index.ts +32 -13
  17. package/src/extension/public-execution.ts +71 -0
  18. package/src/extension/rpc.ts +7 -8
  19. package/src/extension/schemas.ts +14 -14
  20. package/src/extension/tool-description.ts +14 -14
  21. package/src/missions/actions.ts +47 -11
  22. package/src/missions/goal-driver.ts +162 -0
  23. package/src/missions/lifecycle.ts +44 -12
  24. package/src/missions/store.ts +68 -3
  25. package/src/missions/types.ts +25 -3
  26. package/src/missions/workflow-state.ts +250 -0
  27. package/src/profiles/profiles.ts +1 -3
  28. package/src/runs/background/async-execution.ts +5 -1
  29. package/src/runs/background/async-job-tracker.ts +2 -17
  30. package/src/runs/background/async-status.ts +2 -0
  31. package/src/runs/background/control-channel.ts +50 -6
  32. package/src/runs/background/retained-children.ts +68 -0
  33. package/src/runs/background/run-id-resolver.ts +71 -0
  34. package/src/runs/background/run-status.ts +3 -2
  35. package/src/runs/background/scheduled-runs.ts +17 -13
  36. package/src/runs/background/steering.ts +7 -5
  37. package/src/runs/background/subagent-runner.ts +50 -23
  38. package/src/runs/foreground/async-steering-action.ts +36 -10
  39. package/src/runs/foreground/chain-clarify.ts +7 -3
  40. package/src/runs/foreground/execution.ts +29 -19
  41. package/src/runs/foreground/subagent-executor.ts +282 -77
  42. package/src/runs/shared/acceptance.ts +154 -9
  43. package/src/runs/shared/subagent-prompt-runtime.ts +97 -25
  44. package/src/shared/settings.ts +10 -6
  45. package/src/shared/types.ts +22 -2
  46. package/src/slash/delegation-adapters.ts +0 -13
  47. package/src/slash/prompt-template-bridge.ts +14 -12
  48. package/src/slash/prompt-workflows.ts +10 -4
  49. package/src/slash/slash-bridge.ts +8 -6
  50. package/src/slash/slash-commands.ts +24 -9
  51. package/src/slash/slash-live-state.ts +4 -2
  52. package/src/tui/fleet-status.ts +8 -4
  53. package/src/tui/fleet.ts +15 -5
  54. package/src/tui/render.ts +10 -29
  55. package/src/workflows/scripted-workflow.ts +91 -6
  56. package/agents/context-builder.md +0 -46
  57. package/agents/planner.md +0 -56
  58. package/prompts/parallel-context-build.md +0 -55
  59. package/prompts/parallel-handoff-plan.md +0 -61
@@ -19,11 +19,14 @@ import {
19
19
  } from "./foreground-control.ts";
20
20
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
21
21
  import { handleManagementAction } from "../../agents/agent-management.ts";
22
+ import { handleRefinementAction } from "../../agents/agent-refinements.ts";
22
23
  import { buildDoctorReport } from "../../extension/doctor.ts";
24
+ import { normalizePublicSubagentExecution } from "../../extension/public-execution.ts";
23
25
  import { runSync } from "./execution.ts";
24
26
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
25
27
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
26
28
  import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
29
+ import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
27
30
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
28
31
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
29
32
  import { recordRun } from "../shared/run-history.ts";
@@ -49,7 +52,7 @@ import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsy
49
52
  import type { ScheduledRunAction } from "../background/scheduled-runs.ts";
50
53
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
51
54
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
52
- import { validateExecutionAcceptance } from "../shared/acceptance.ts";
55
+ import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
53
56
  import { createForkContextResolver, forkedChildRequiresThinkingOff } from "../../shared/fork-context.ts";
54
57
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
55
58
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
@@ -75,8 +78,8 @@ import {
75
78
  stripDetailsOutputsForIntercomReceipt,
76
79
  } from "../../intercom/result-intercom.ts";
77
80
  import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
78
- import { deliverCheckpointDecisionRequest, deliverInterruptRequest, requestAsyncSteer } from "../background/control-channel.ts";
79
- import { waitForSteeringAction } from "../background/steering.ts";
81
+ import { deliverCheckpointDecisionRequest, deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
82
+ import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
80
83
  import { steerAsyncRun } from "./async-steering-action.ts";
81
84
  import { stopAsyncRun } from "./async-stop-action.ts";
82
85
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
@@ -88,10 +91,12 @@ import { inspectSubagentStatus } from "../background/run-status.ts";
88
91
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
89
92
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
90
93
  import { attachMissionToLaunchResult, prepareMissionLaunch, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
94
+ import { updateMission } from "../../missions/store.ts";
95
+ import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
91
96
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
92
97
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
93
98
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
94
- import { runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
99
+ import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
95
100
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
96
101
  import {
97
102
  cleanupWorktrees,
@@ -144,7 +149,7 @@ import {
144
149
  wrapForkTask,
145
150
  } from "../../shared/types.ts";
146
151
 
147
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
152
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
148
153
 
149
154
  type UndefinedOmitted<T extends object> = {
150
155
  [K in keyof T as undefined extends T[K] ? never : K]: T[K];
@@ -209,8 +214,11 @@ export interface SubagentParamsLike {
209
214
  type?: string;
210
215
  agent?: string;
211
216
  task?: string;
217
+ /** Retained async child run id. Valid only on workflow runs.run items. */
218
+ resume?: string;
212
219
  message?: string;
213
220
  steeringRecovery?: boolean;
221
+ mode?: SteerDeliveryMode;
214
222
  workflowScript?: string;
215
223
  chatProgress?: "auto" | "off" | "live-card";
216
224
  step?: ChainStep;
@@ -253,6 +261,7 @@ export interface SubagentParamsLike {
253
261
  agentScope?: unknown;
254
262
  chainDir?: string;
255
263
  acceptance?: AcceptanceInput;
264
+ gate?: string;
256
265
  agentContract?: AgentContract;
257
266
  schedule?: string;
258
267
  scheduleName?: string;
@@ -790,6 +799,20 @@ function getAsyncInterruptTarget(
790
799
  return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
791
800
  }
792
801
 
802
+ function isStaleExtensionContextError(error: unknown): boolean {
803
+ if (!(error instanceof Error)) return false;
804
+ return /extension ctx is stale|stale after session replacement or reload/i.test(error.message);
805
+ }
806
+
807
+ function emitAdvisoryControlEvent(pi: ExtensionAPI, channel: string, payload: unknown): void {
808
+ try {
809
+ pi.events.emit(channel, payload);
810
+ } catch (error) {
811
+ if (isStaleExtensionContextError(error)) return;
812
+ throw error;
813
+ }
814
+ }
815
+
793
816
  function emitControlNotification(input: {
794
817
  pi: ExtensionAPI;
795
818
  controlConfig: ResolvedControlConfig;
@@ -807,10 +830,10 @@ function emitControlNotification(input: {
807
830
  noticeText: formatControlNoticeMessage(input.event, childIntercomTarget),
808
831
  };
809
832
  if (input.controlConfig.notifyChannels.includes("event")) {
810
- input.pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
833
+ emitAdvisoryControlEvent(input.pi, SUBAGENT_CONTROL_EVENT, payload);
811
834
  }
812
835
  if (input.event.type !== "active_long_running" && input.controlConfig.notifyChannels.includes("intercom") && input.intercomBridge.active && input.intercomBridge.orchestratorTarget) {
813
- input.pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
836
+ emitAdvisoryControlEvent(input.pi, SUBAGENT_CONTROL_INTERCOM_EVENT, {
814
837
  ...payload,
815
838
  to: input.intercomBridge.orchestratorTarget,
816
839
  message: formatControlIntercomMessage(input.event, childIntercomTarget),
@@ -1160,7 +1183,7 @@ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nes
1160
1183
  }
1161
1184
  }
1162
1185
 
1163
- async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details> | undefined> {
1186
+ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; mode?: SteerDeliveryMode; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details> | undefined> {
1164
1187
  const run = input.target.match.run;
1165
1188
  const asyncDir = resolveNestedAsyncDir(input.target.match.rootRunId, run);
1166
1189
  if (!asyncDir) return undefined;
@@ -1182,6 +1205,7 @@ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & {
1182
1205
  try {
1183
1206
  requestAsyncSteer(asyncDir, {
1184
1207
  message: input.message,
1208
+ mode: input.mode,
1185
1209
  ...(effectiveTargetIndex !== undefined ? { targetIndex: effectiveTargetIndex } : { targetIndexes }),
1186
1210
  source: "nested-steer",
1187
1211
  id: requestId,
@@ -1191,12 +1215,12 @@ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & {
1191
1215
  }
1192
1216
  const targets = targetIndexes.map((index) => ({ index, state: steps[index]?.status === "pending" ? "scheduled" as const : "pending" as const }));
1193
1217
  if (targets.every((target) => target.state === "scheduled")) {
1194
- const scheduled = { requestId, state: "scheduled" as const, sourceRunId: run.id, targets };
1218
+ const scheduled = { requestId, state: "scheduled" as const, deliveryStatus: "queued" as const, sourceRunId: run.id, targets };
1195
1219
  return { content: [{ type: "text", text: `Steering scheduled for nested async run ${run.id} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } };
1196
1220
  }
1197
1221
  const waited = await waitForSteeringAction(omitUndefinedProperties({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal }));
1198
- const result = waited ?? { requestId, state: "pending" as const, sourceRunId: run.id, targets };
1199
- const stateText = result.state === "delivered" ? "delivered" : result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : "pending";
1222
+ const result = waited ?? { requestId, state: "pending" as const, deliveryStatus: "queued" as const, sourceRunId: run.id, targets };
1223
+ const stateText = result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : result.deliveryStatus === "queued" ? "queued" : result.state === "delivered" ? "delivered" : "pending";
1200
1224
  return { content: [{ type: "text", text: `Steering ${stateText} for nested async run ${run.id} (request ${requestId}).` }], ...(result.state === "failed" || result.state === "partial" ? { isError: true } : {}), details: { mode: "management", results: [], steering: result } };
1201
1225
  }
1202
1226
 
@@ -1219,7 +1243,7 @@ async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { ki
1219
1243
  return { content: [{ type: "text", text: `Nested run ${run.id} appears live but its owner route is not reachable. Wait for completion, then retry action='resume'.` }], isError: true, details: { mode: "management", results: [] } };
1220
1244
  }
1221
1245
 
1222
- async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details>> {
1246
+ async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; mode?: SteerDeliveryMode; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details>> {
1223
1247
  const run = input.target.match.run;
1224
1248
  if (run.state !== "running" && run.state !== "queued") return { content: [{ type: "text", text: `Nested run ${run.id} is ${run.state} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] } };
1225
1249
  const direct = await directNestedAsyncSteer(input);
@@ -1449,6 +1473,9 @@ async function resumeAsyncRun(input: {
1449
1473
  return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }], details: result.details };
1450
1474
  }
1451
1475
 
1476
+ const sourceAsyncDir = target.source === "async" ? target.asyncDir : undefined;
1477
+ const queuedBriefs = sourceAsyncDir ? readRevivalBriefs(sourceAsyncDir) : [];
1478
+ const effectiveFollowUp = [...queuedBriefs.map(({ request }) => request.message), followUp].filter(Boolean).join("\n\n");
1452
1479
  const revivalSessionFile = target.sessionFile;
1453
1480
  if (!revivalSessionFile) {
1454
1481
  return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
@@ -1461,8 +1488,8 @@ async function resumeAsyncRun(input: {
1461
1488
  const parentModel = input.parentModel;
1462
1489
  const result = executeAsyncSingle(runId, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
1463
1490
  agent: target.agent,
1464
- task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], followUp),
1465
- goal: followUp,
1491
+ task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], effectiveFollowUp),
1492
+ goal: effectiveFollowUp,
1466
1493
  agentConfig: recoveryAgentConfig,
1467
1494
  ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1468
1495
  pi: input.deps.pi,
@@ -1512,8 +1539,18 @@ async function resumeAsyncRun(input: {
1512
1539
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1513
1540
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1514
1541
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1542
+ parentWorkflowRunId: input.params.workflowParentRunId,
1543
+ workflowKey: input.params.workflowKey,
1515
1544
  }));
1516
1545
  if (result.isError) return result;
1546
+ for (const brief of queuedBriefs) fs.rmSync(brief.path, { force: true });
1547
+ if (queuedBriefs.length > 0 && sourceAsyncDir) {
1548
+ const sourceStatus = readStatus(sourceAsyncDir);
1549
+ if (sourceStatus?.steering) {
1550
+ for (const brief of queuedBriefs) updateSteeringTarget(sourceStatus.steering, brief.request.id, target.index, "delivered", Date.now());
1551
+ writeAtomicJson(path.join(sourceAsyncDir, "status.json"), sourceStatus);
1552
+ }
1553
+ }
1517
1554
 
1518
1555
  const revivedId = result.details.asyncId ?? runId;
1519
1556
  const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
@@ -1843,6 +1880,19 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1843
1880
  return "single";
1844
1881
  }
1845
1882
 
1883
+ function formatStatusTargetLabel(params: Pick<SubagentParamsLike, "dir" | "index" | "view">, targetRunId: string | undefined): string {
1884
+ let target: string;
1885
+ if (targetRunId) {
1886
+ target = `run ${targetRunId}`;
1887
+ } else if (params.dir) {
1888
+ target = `dir ${params.dir}`;
1889
+ } else {
1890
+ target = params.view === "transcript" ? "active run" : "active runs";
1891
+ }
1892
+ if (params.view !== "transcript") return `Status target: ${target}`;
1893
+ return `Transcript target: ${target}${params.index !== undefined ? ` · child ${params.index}` : ""}`;
1894
+ }
1895
+
1846
1896
  interface AgentDefaultContextPolicy {
1847
1897
  params: SubagentParamsLike;
1848
1898
  contextForAgent(agentName: string): ContextMode;
@@ -3301,10 +3351,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3301
3351
  );
3302
3352
  if (errorResult) return errorResult;
3303
3353
 
3304
- let worktreeFinalized = false;
3354
+ let worktreeCleanupHandled = false;
3355
+ let pendingHandoff: Details["parallelHandoff"];
3305
3356
  try {
3306
3357
  if (worktreeSetup) {
3307
- writePendingParallelHandoff({
3358
+ pendingHandoff = writePendingParallelHandoff({
3308
3359
  manifestPath: parallelHandoffPath(artifactsDir, runId),
3309
3360
  runId,
3310
3361
  mode: "parallel",
@@ -3407,10 +3458,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3407
3458
  updateForegroundNestedProjection(foregroundControl);
3408
3459
  attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
3409
3460
  }
3461
+ const detached = results.find((result) => result.detached);
3410
3462
  let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
3411
3463
  if (worktreeSetup) {
3412
- worktreeFinalized = true;
3413
- handoff = finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
3464
+ worktreeCleanupHandled = true;
3465
+ handoff = detached
3466
+ ? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
3467
+ : finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
3414
3468
  }
3415
3469
  const interrupted = results.find((result) => result.interrupted);
3416
3470
  const totalCost = sumResultsCost(results);
@@ -3433,11 +3487,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3433
3487
  details,
3434
3488
  };
3435
3489
  }
3436
- const detachedIndex = results.findIndex((result) => result.detached);
3437
- const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
3438
3490
  if (detached) {
3491
+ const handoffSuffix = handoff?.suffix ? `\n\n${handoff.suffix}` : "";
3439
3492
  return {
3440
- content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
3493
+ content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.${handoffSuffix}` }],
3441
3494
  details,
3442
3495
  };
3443
3496
  }
@@ -3486,7 +3539,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3486
3539
  details,
3487
3540
  };
3488
3541
  } finally {
3489
- if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
3542
+ if (worktreeSetup && !worktreeCleanupHandled) cleanupWorktrees(worktreeSetup);
3490
3543
  }
3491
3544
  }
3492
3545
 
@@ -3873,6 +3926,8 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3873
3926
  const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
3874
3927
  ? result.details.results[0].finalOutput
3875
3928
  : receiptOutput;
3929
+ const detached = result.details.results.some((child) => child.detached);
3930
+ const ok = result.isError !== true && !detached;
3876
3931
  const artifactPaths = new Set<string>();
3877
3932
  if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
3878
3933
  if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
@@ -3884,16 +3939,70 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3884
3939
  const structured = result.details.results.map((child) => child.structuredOutput).filter((value) => value !== undefined);
3885
3940
  return {
3886
3941
  key,
3887
- ok: result.isError !== true,
3942
+ ok,
3888
3943
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3889
3944
  output,
3890
- ...(result.isError === true ? { error: receiptOutput || output || "Child run failed." } : {}),
3945
+ ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
3891
3946
  ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
3892
3947
  artifactPaths: [...artifactPaths],
3893
3948
  results: result.details.results,
3894
3949
  };
3895
3950
  }
3896
3951
 
3952
+ export function prepareWorkflowLaunchParams(
3953
+ workflowDefaults: SubagentParamsLike,
3954
+ childParams: Record<string, unknown>,
3955
+ parentWorkflowRunId: string,
3956
+ workflowKey: string,
3957
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean } = {},
3958
+ ): SubagentParamsLike {
3959
+ if (typeof childParams.resume === "string") {
3960
+ if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
3961
+ throw new Error("gate is not supported with retained resume; resume uses the retained child contract.");
3962
+ }
3963
+ const timeoutMs = childParams.timeoutMs ?? childParams.maxRuntimeMs ?? workflowDefaults.timeoutMs ?? workflowDefaults.maxRuntimeMs;
3964
+ const turnBudget = childParams.turnBudget ?? workflowDefaults.turnBudget;
3965
+ const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
3966
+ return {
3967
+ action: "resume",
3968
+ id: childParams.resume.trim(),
3969
+ message: typeof childParams.task === "string" ? childParams.task.trim() : "",
3970
+ workflowParentRunId: parentWorkflowRunId,
3971
+ workflowKey,
3972
+ ...(options.missionDetached ? { mission: false } : {}),
3973
+ ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
3974
+ ...(turnBudget !== undefined ? { turnBudget: turnBudget as TurnBudgetConfig } : {}),
3975
+ ...(toolBudget !== undefined ? { toolBudget: toolBudget as ToolBudgetConfig } : {}),
3976
+ };
3977
+ }
3978
+ const launchParams = {
3979
+ ...workflowDefaults,
3980
+ ...childParams,
3981
+ ...(options.missionDetached ? { mission: false } : {}),
3982
+ workflowParentRunId: parentWorkflowRunId,
3983
+ workflowKey,
3984
+ ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
3985
+ } as SubagentParamsLike;
3986
+ const normalizedGate = normalizeGateParams(launchParams);
3987
+ if (!normalizedGate.ok) throw new Error(normalizedGate.error);
3988
+ return prepareWorkflowChildParams(normalizedGate.params);
3989
+ }
3990
+
3991
+ type GateParamsNormalizationResult =
3992
+ | { ok: true; params: SubagentParamsLike }
3993
+ | { ok: false; error: string };
3994
+
3995
+ function normalizeGateParams(params: SubagentParamsLike): GateParamsNormalizationResult {
3996
+ if (params.gate !== undefined && params.action === "resume") {
3997
+ return { ok: false, error: "gate is not supported with action='resume'; resume uses the retained child contract." };
3998
+ }
3999
+ const normalized = normalizeGateAcceptance(params.gate, params.acceptance);
4000
+ if (!normalized.ok) return { ok: false, error: normalized.error };
4001
+ if (params.gate === undefined) return { ok: true, params };
4002
+ const { gate: _gate, ...rest } = params;
4003
+ return { ok: true, params: { ...rest, ...(normalized.acceptance !== undefined ? { acceptance: normalized.acceptance } : {}) } };
4004
+ }
4005
+
3897
4006
  function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsLike {
3898
4007
  if (params.worktree !== true || !params.agent) return params;
3899
4008
  const {
@@ -3949,21 +4058,6 @@ function workflowChatProgressUpdate(
3949
4058
  };
3950
4059
  }
3951
4060
 
3952
- function omitExecutionModeActionAlias(params: SubagentParamsLike): SubagentParamsLike {
3953
- const action = params.action?.toLowerCase();
3954
- if (action === "single" && (params.agent !== undefined || params.task !== undefined)) {
3955
- const rest = { ...params };
3956
- delete rest.action;
3957
- return rest;
3958
- }
3959
- if ((action === "parallel" || action === "tasks") && (params.tasks?.length ?? 0) > 0) {
3960
- const rest = { ...params };
3961
- delete rest.action;
3962
- return rest;
3963
- }
3964
- return params;
3965
- }
3966
-
3967
4061
  function createScheduledOwnerState(source: SubagentState, ownerSessionId: string, ctx: ExtensionContext): SubagentState {
3968
4062
  const ownerSpawns = source.subagentSpawns?.sessionId === ownerSessionId
3969
4063
  ? {
@@ -4002,6 +4096,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4002
4096
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
4003
4097
  ctx: ExtensionContext,
4004
4098
  ) => Promise<AgentToolResult<Details>>;
4099
+ /** Public/model-facing execution boundary. Internal direct launch primitives use execute or executeDelegated. */
4100
+ executePublic: (
4101
+ id: string,
4102
+ params: SubagentParamsLike,
4103
+ signal: AbortSignal,
4104
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
4105
+ ctx: ExtensionContext,
4106
+ ) => Promise<AgentToolResult<Details>>;
4005
4107
  /**
4006
4108
  * Correlated extension-to-extension delegation owns its request IDs and
4007
4109
  * cancellation controllers, so independent requests may execute concurrently.
@@ -4040,24 +4142,69 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4040
4142
  deps.state.foregroundRuns ??= new Map();
4041
4143
  deps.state.foregroundControls ??= new Map();
4042
4144
  deps.state.lastForegroundControlId ??= null;
4043
- const requestParams = omitExecutionModeActionAlias(params);
4044
- if (requestParams.workflowScript !== undefined) {
4045
- const invalidMode = requestParams.action !== undefined || requestParams.agent !== undefined || requestParams.step !== undefined || requestParams.tasks !== undefined || requestParams.chain !== undefined;
4046
- if (invalidMode) {
4047
- return { content: [{ type: "text", text: "workflowScript is its own execution mode; do not combine it with action, agent, step, tasks, or chain." }], isError: true, details: { mode: "workflow", results: [] } };
4048
- }
4049
- if (requestParams.clarify === true) {
4050
- return { content: [{ type: "text", text: "workflowScript does not support clarify UI." }], isError: true, details: { mode: "workflow", results: [] } };
4051
- }
4145
+ const normalizedGate = normalizeGateParams(params);
4146
+ if (!normalizedGate.ok) return buildRequestedModeError(params, normalizedGate.error);
4147
+ const requestParams = normalizedGate.params;
4148
+ const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
4149
+ if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4150
+ const parentCwd = ctx.cwd;
4052
4151
  const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
4053
4152
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4054
4153
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4055
- const workflowCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd);
4056
- const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd: ctx.cwd, workflowCwd, background: requestParams.async !== false });
4154
+ const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4155
+ const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4057
4156
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4058
4157
  const chatProgress = chatProgressResult.projection!;
4158
+ const explicitMission = requestParams.missionId !== undefined || requestParams.mission !== undefined;
4159
+ const autoMission = !explicitMission;
4160
+ const workflowPreview = autoMission ? previewSimpleWorkflowRun(requestParams.workflowScript) : undefined;
4161
+ const previewTask = workflowPreview?.task?.trim() || undefined;
4162
+ const previewAgent = workflowPreview?.agent?.trim() || undefined;
4163
+ const scriptFirstLine = requestParams.workflowScript.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Workflow";
4164
+ const boundedScriptPreview = scriptFirstLine.length > 100 ? `${scriptFirstLine.slice(0, 97)}...` : scriptFirstLine;
4165
+ const derivedObjective = previewTask || (previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview);
4166
+ let missionBinding: MissionLaunchBinding | undefined;
4167
+ let missionWarning: string | undefined;
4168
+ try {
4169
+ missionBinding = prepareMissionLaunch({
4170
+ params: autoMission ? { ...requestParams, task: derivedObjective } : requestParams,
4171
+ projectRoot: workflowCwd,
4172
+ ...(deps.config.missions ? { config: deps.config.missions } : {}),
4173
+ ownerSessionId: resolveCurrentSessionId(ctx.sessionManager),
4174
+ });
4175
+ } catch (error) {
4176
+ if (explicitMission) return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4177
+ missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4178
+ }
4179
+ let shouldPatchMissionObjective = autoMission && previewTask === undefined && missionBinding !== undefined;
4180
+ const patchMissionObjective = (task: unknown): void => {
4181
+ if (!shouldPatchMissionObjective || !missionBinding || typeof task !== "string" || !task.trim()) return;
4182
+ shouldPatchMissionObjective = false;
4183
+ const objective = task.trim();
4184
+ const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective;
4185
+ const title = firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
4186
+ try {
4187
+ updateMission(missionBinding.location, missionBinding.missionId, { title, objective });
4188
+ } catch (error) {
4189
+ console.warn(`[pi-subagents] Failed to update automatic mission objective: ${error instanceof Error ? error.message : String(error)}`);
4190
+ }
4191
+ };
4192
+ const detachWorkflowChildMissions = autoMission || missionBinding !== undefined || requestParams.mission === false;
4193
+ const workflowState = missionBinding ? createMissionWorkflowState(missionBinding.location, missionBinding.missionId) : undefined;
4194
+ const attachWorkflowMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
4195
+ if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
4196
+ try {
4197
+ return attachMissionToLaunchResult({ binding: missionBinding, result });
4198
+ } catch (error) {
4199
+ const warning = `Mission tracking unavailable after launch: ${error instanceof Error ? error.message : String(error)}`;
4200
+ return explicitMission
4201
+ ? { ...result, isError: true, content: [...result.content, { type: "text", text: warning }], details: { ...result.details, missionWarning: warning } }
4202
+ : { ...result, details: { ...result.details, missionWarning: warning } };
4203
+ }
4204
+ };
4059
4205
  if (requestParams.async !== false) {
4060
- const workflowRunId = _id;
4206
+ const toolCallId = _id;
4207
+ const workflowRunId = randomUUID();
4061
4208
  const asyncDir = path.join(DIRS.async, workflowRunId);
4062
4209
  const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4063
4210
  const statusPath = path.join(asyncDir, "status.json");
@@ -4071,13 +4218,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4071
4218
  deps.state.workflowControllers.set(workflowRunId, controller);
4072
4219
  let status: AsyncStatus = {
4073
4220
  runId: workflowRunId,
4221
+ toolCallId,
4074
4222
  sessionId: currentSessionId ?? undefined,
4075
4223
  mode: "workflow",
4076
4224
  state: "running",
4077
4225
  startedAt,
4078
4226
  lastUpdate: startedAt,
4079
4227
  ...(timeout !== undefined ? { deadlineAt: startedAt + timeout, timeoutMs: timeout } : {}),
4080
- cwd: ctx.cwd,
4228
+ cwd: workflowCwd,
4081
4229
  pid: process.pid,
4082
4230
  steps: [],
4083
4231
  workflow: { trace: [], emits: [], console: [] },
@@ -4100,7 +4248,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4100
4248
  job.workflow = status.workflow;
4101
4249
  }
4102
4250
  };
4103
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: ctx.cwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4251
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4104
4252
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
4105
4253
  deps.state.fleetJobs ??= new Map();
4106
4254
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
@@ -4109,7 +4257,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4109
4257
  const { workflowScript, async: _workflowAsync, chatProgress: _chatProgress, ...workflowRequest } = requestParams;
4110
4258
  void Promise.resolve().then(async () => {
4111
4259
  const workflowResults: SingleResult[] = [];
4112
- const { action: _action, agent: _agent, task: _task, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, ...workflowChildDefaults } = workflowRequest;
4260
+ 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;
4113
4261
  const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
4114
4262
  status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
4115
4263
  for (const entry of trace.filter((candidate) => candidate.operation === "run")) {
@@ -4134,6 +4282,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4134
4282
  script: workflowScript,
4135
4283
  timeoutMs: timeout,
4136
4284
  signal: controller.signal,
4285
+ ...(workflowState ? { state: workflowState } : {}),
4137
4286
  onTrace: updateTrace,
4138
4287
  onEmit: (emits) => {
4139
4288
  // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
@@ -4145,7 +4294,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4145
4294
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4146
4295
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4147
4296
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4148
- const childRequest = prepareWorkflowChildParams({ ...workflowChildDefaults, ...childParams, workflowParentRunId: workflowRunId, workflowKey: key } as SubagentParamsLike);
4297
+ patchMissionObjective(childParams.task);
4298
+ const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions });
4149
4299
  const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4150
4300
  workflowResults.push(...result.details.results);
4151
4301
  const child = workflowChildResult(key, result);
@@ -4164,24 +4314,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4164
4314
  status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4165
4315
  persist();
4166
4316
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4167
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: ctx.cwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4317
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4168
4318
  } catch (error) {
4169
4319
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4170
4320
  const stopped = controller.signal.aborted;
4171
4321
  status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
4172
4322
  persist();
4173
4323
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4174
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: ctx.cwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4324
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4175
4325
  } finally {
4176
4326
  deps.state.workflowControllers?.delete(workflowRunId);
4177
4327
  }
4178
4328
  });
4179
- return {
4329
+ return attachWorkflowMission({
4180
4330
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4181
- details: { mode: "workflow", runId: workflowRunId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4182
- };
4331
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4332
+ });
4183
4333
  }
4184
- const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, ...workflowChildDefaults } = requestParams;
4334
+ 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;
4185
4335
  const workflowResults: SingleResult[] = [];
4186
4336
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
4187
4337
  const sendWorkflowProgress = () => {
@@ -4193,6 +4343,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4193
4343
  script: requestParams.workflowScript,
4194
4344
  timeoutMs: timeout,
4195
4345
  signal,
4346
+ ...(workflowState ? { state: workflowState } : {}),
4196
4347
  onTrace: (trace) => {
4197
4348
  liveWorkflow = { ...liveWorkflow, trace };
4198
4349
  sendWorkflowProgress();
@@ -4205,7 +4356,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4205
4356
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4206
4357
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4207
4358
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4208
- const childRequest = prepareWorkflowChildParams({ ...workflowChildDefaults, ...childParams, workflowParentRunId: _id, workflowKey: key, suppressRoutineResultIntercom: chatProgress.mode === "live-card" } as SubagentParamsLike);
4359
+ patchMissionObjective(childParams.task);
4360
+ const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" });
4209
4361
  const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4210
4362
  workflowResults.push(...result.details.results);
4211
4363
  return workflowChildResult(key, result);
@@ -4217,10 +4369,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4217
4369
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
4218
4370
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4219
4371
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4220
- return {
4372
+ return attachWorkflowMission({
4221
4373
  content: [{ type: "text", text: sections.join("\n\n") }],
4222
4374
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), 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 }, chatProgress }),
4223
- };
4375
+ });
4224
4376
  } catch (error) {
4225
4377
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4226
4378
  const text = error instanceof Error ? error.message : String(error);
@@ -4229,11 +4381,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4229
4381
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
4230
4382
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4231
4383
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4232
- return {
4384
+ return attachWorkflowMission({
4233
4385
  content: [{ type: "text", text: sections.join("\n\n") }],
4234
4386
  isError: true,
4235
4387
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console }, chatProgress }),
4236
- };
4388
+ });
4237
4389
  }
4238
4390
  }
4239
4391
  const directParams = prepareWorkflowChildParams(requestParams);
@@ -4337,6 +4489,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4337
4489
  }
4338
4490
  return handleWatchdogToolAction(action, paramsWithResolvedCwd, ctx, deps.watchdog);
4339
4491
  }
4492
+ if (action === "refine" || action === "refine.show" || action === "refine.rollback") {
4493
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4494
+ return {
4495
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
4496
+ isError: true,
4497
+ details: { mode: "management", results: [] },
4498
+ };
4499
+ }
4500
+ return handleRefinementAction(action, paramsWithResolvedCwd, {
4501
+ cwd: requestCwd,
4502
+ state: deps.state,
4503
+ signal,
4504
+ launchProposalChild: (task, outputSchema, proposalSignal) => execute(randomUUID(), {
4505
+ agent: "reviewer",
4506
+ task,
4507
+ context: "fresh",
4508
+ async: false,
4509
+ artifacts: false,
4510
+ outputSchema,
4511
+ toolBudget: { hard: 1, block: ["write", "edit", "bash"] },
4512
+ }, proposalSignal, undefined, ctx, true),
4513
+ });
4514
+ }
4340
4515
  if (action === "grant-spawn-budget") {
4341
4516
  if (deps.allowMutatingManagementActions === false || !ctx.hasUI) {
4342
4517
  return {
@@ -4409,6 +4584,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4409
4584
  details: { mode: "management", results: [], spawnBudget: granted.snapshot },
4410
4585
  };
4411
4586
  }
4587
+ if (action === "children.list") {
4588
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4589
+ const children = listRetainedChildren(DIRS.async, deps.state.currentSessionId);
4590
+ return {
4591
+ content: [{ type: "text", text: formatRetainedChildren(children) }],
4592
+ details: { mode: "management", results: [] },
4593
+ };
4594
+ }
4412
4595
  if (action === "doctor") {
4413
4596
  let currentSessionFile: string | null = null;
4414
4597
  let currentSessionId = deps.state.currentSessionId;
@@ -4447,13 +4630,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4447
4630
  }
4448
4631
  if (action === "status") {
4449
4632
  if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4450
- const withBudget = (result: AgentToolResult<Details>) => withSpawnBudgetStatus(
4451
- result,
4452
- deps.state,
4453
- deps.config,
4454
- deps.state.currentSessionId,
4455
- );
4456
4633
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
4634
+ const hasDirectoryTarget = Boolean(paramsWithResolvedCwd.dir);
4635
+ const targetLabel = formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
4636
+ const withBudget = (result: AgentToolResult<Details>) => {
4637
+ const budgeted = withSpawnBudgetStatus(result, deps.state, deps.config, deps.state.currentSessionId);
4638
+ return {
4639
+ ...budgeted,
4640
+ content: budgeted.content.map((item, index) => index === 0 && item.type === "text"
4641
+ ? { ...item, text: `${targetLabel}\n${item.text}` }
4642
+ : item),
4643
+ };
4644
+ };
4457
4645
  const nestedScope = nestedResolutionScopeForExecutor(deps);
4458
4646
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
4459
4647
  if (paramsWithResolvedCwd.view === "fleet") {
@@ -4478,7 +4666,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4478
4666
  const message = error instanceof Error ? error.message : String(error);
4479
4667
  return withBudget({ content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } });
4480
4668
  }
4481
- } else {
4669
+ } else if (!hasDirectoryTarget) {
4482
4670
  const foreground = getForegroundControl(deps.state, undefined);
4483
4671
  if (foreground && paramsWithResolvedCwd.view !== "transcript") return withBudget(foregroundStatusResult(foreground));
4484
4672
  if (foreground && paramsWithResolvedCwd.view === "transcript") {
@@ -4565,6 +4753,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4565
4753
  state: deps.state,
4566
4754
  runId,
4567
4755
  message,
4756
+ mode: paramsWithResolvedCwd.mode,
4568
4757
  index: paramsWithResolvedCwd.index,
4569
4758
  kill: deps.kill,
4570
4759
  location,
@@ -4590,7 +4779,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4590
4779
  const text = error instanceof Error ? error.message : String(error);
4591
4780
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
4592
4781
  }
4593
- if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, index: paramsWithResolvedCwd.index, signal }));
4782
+ if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal }));
4594
4783
  if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
4595
4784
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
4596
4785
  if (resolved.location.asyncDir) {
@@ -4601,6 +4790,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4601
4790
  state: deps.state,
4602
4791
  runId: resolved.id,
4603
4792
  message,
4793
+ mode: paramsWithResolvedCwd.mode,
4604
4794
  index: paramsWithResolvedCwd.index,
4605
4795
  kill: deps.kill,
4606
4796
  location: resolved.location,
@@ -5204,8 +5394,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5204
5394
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
5205
5395
  ctx: ExtensionContext,
5206
5396
  ): Promise<AgentToolResult<Details>> => {
5207
- const requestParams = omitExecutionModeActionAlias(params);
5208
- if (requestParams.action) return execute(id, requestParams, signal, onUpdate, ctx);
5397
+ const normalizedAction = typeof params.action === "string" ? params.action.trim() : params.action;
5398
+ const requestParams = normalizedAction ? { ...params, action: normalizedAction } : params;
5399
+ if (normalizedAction) return execute(id, requestParams, signal, onUpdate, ctx);
5209
5400
  const { depth } = checkSubagentDepth(deps.config.maxSubagentDepth);
5210
5401
  const dispatchParams = applyForceTopLevelAsyncOverride(requestParams, depth, deps.config.forceTopLevelAsync === true);
5211
5402
  const runsForeground = dispatchParams.clarify === true || (dispatchParams.async ?? deps.asyncByDefault) !== true;
@@ -5219,6 +5410,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5219
5410
  }
5220
5411
  };
5221
5412
 
5413
+ const executePublic = (
5414
+ id: string,
5415
+ params: SubagentParamsLike,
5416
+ signal: AbortSignal,
5417
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
5418
+ ctx: ExtensionContext,
5419
+ ): Promise<AgentToolResult<Details>> => {
5420
+ const normalized = normalizePublicSubagentExecution(params);
5421
+ if (!normalized.ok) {
5422
+ return Promise.resolve({ content: [{ type: "text", text: normalized.error }], isError: true, details: { mode: normalized.mode, results: [] } });
5423
+ }
5424
+ return executeWithSingleDispatchGuard(id, normalized.params, signal, onUpdate, ctx);
5425
+ };
5426
+
5222
5427
  const executeDelegated = async (
5223
5428
  id: string,
5224
5429
  params: SubagentParamsLike,
@@ -5258,5 +5463,5 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5258
5463
  return ownerExecutor.execute(id, params, signal, undefined, ctx);
5259
5464
  };
5260
5465
 
5261
- return { execute: executeWithSingleDispatchGuard, executeDelegated, executeScheduled };
5466
+ return { execute: executeWithSingleDispatchGuard, executePublic, executeDelegated, executeScheduled };
5262
5467
  }