pi-subagents 0.56.0 → 0.58.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 (108) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +124 -21
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +41 -2
  11. package/docs/models.md +3 -3
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +14 -3
  14. package/docs/workflows.md +44 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/SKILL.md +2 -0
  17. package/skills/pi-subagents/references/execution-controls.md +2 -2
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  19. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  20. package/src/agents/agent-management.ts +36 -5
  21. package/src/agents/agent-refinements.ts +4 -4
  22. package/src/agents/agent-serializer.ts +5 -0
  23. package/src/agents/agents.ts +257 -51
  24. package/src/agents/builtin-names.ts +6 -0
  25. package/src/agents/runtime-agent-events.ts +70 -0
  26. package/src/agents/runtime-agent-registry.ts +18 -4
  27. package/src/api/agents.ts +10 -5
  28. package/src/api/preflight.ts +28 -3
  29. package/src/extension/config.ts +70 -0
  30. package/src/extension/doctor.ts +3 -3
  31. package/src/extension/index.ts +33 -8
  32. package/src/extension/public-execution.ts +29 -13
  33. package/src/extension/rpc.ts +55 -19
  34. package/src/extension/schemas.ts +6 -5
  35. package/src/extension/tool-description.ts +18 -12
  36. package/src/inspectors/herdr/actions.ts +2 -1
  37. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  38. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  39. package/src/integrations/herdr-status.ts +51 -3
  40. package/src/runs/background/active-async-capacity.ts +77 -10
  41. package/src/runs/background/async-execution.ts +127 -19
  42. package/src/runs/background/async-job-tracker.ts +5 -0
  43. package/src/runs/background/async-resume.ts +6 -2
  44. package/src/runs/background/async-retention.ts +20 -3
  45. package/src/runs/background/async-status.ts +7 -0
  46. package/src/runs/background/chain-append.ts +2 -0
  47. package/src/runs/background/chain-root-attachment.ts +15 -1
  48. package/src/runs/background/fleet-view.ts +16 -10
  49. package/src/runs/background/inspect-rpc.ts +8 -8
  50. package/src/runs/background/notify.ts +26 -3
  51. package/src/runs/background/result-delivery-ownership.ts +45 -0
  52. package/src/runs/background/result-files.ts +27 -14
  53. package/src/runs/background/result-watcher.ts +36 -15
  54. package/src/runs/background/run-status.ts +33 -6
  55. package/src/runs/background/scheduled-runs.ts +7 -1
  56. package/src/runs/background/subagent-runner.ts +239 -56
  57. package/src/runs/background/wait-completions.ts +4 -0
  58. package/src/runs/foreground/execution.ts +92 -11
  59. package/src/runs/foreground/foreground-control.ts +6 -0
  60. package/src/runs/foreground/foreground-history.ts +22 -1
  61. package/src/runs/foreground/subagent-executor.ts +322 -102
  62. package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
  63. package/src/runs/shared/child-protocol.ts +21 -7
  64. package/src/runs/shared/claude-code-adapter.ts +129 -0
  65. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  66. package/src/runs/shared/completion-guard.ts +4 -3
  67. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  68. package/src/runs/shared/dynamic-fanout.ts +3 -3
  69. package/src/runs/shared/external-cli-contract.ts +167 -0
  70. package/src/runs/shared/external-cli-preflight.ts +122 -0
  71. package/src/runs/shared/external-cli-runner.ts +348 -55
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/launch-cwd.ts +16 -0
  74. package/src/runs/shared/long-running-guard.ts +2 -1
  75. package/src/runs/shared/mcp-config-sources.ts +386 -0
  76. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  77. package/src/runs/shared/model-exclusions.ts +69 -7
  78. package/src/runs/shared/model-fallback.ts +39 -5
  79. package/src/runs/shared/mutation-evidence.ts +7 -2
  80. package/src/runs/shared/nested-events.ts +3 -1
  81. package/src/runs/shared/nested-render.ts +2 -2
  82. package/src/runs/shared/parallel-utils.ts +8 -1
  83. package/src/runs/shared/pi-args.ts +61 -6
  84. package/src/runs/shared/process-signal.ts +13 -0
  85. package/src/runs/shared/run-history.ts +21 -1
  86. package/src/runs/shared/single-output.ts +17 -0
  87. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  88. package/src/shared/fork-context.ts +21 -0
  89. package/src/shared/formatters.ts +13 -1
  90. package/src/shared/launch-contract.ts +4 -0
  91. package/src/shared/pruned-fork.ts +450 -0
  92. package/src/shared/session-file-trust.ts +19 -0
  93. package/src/shared/session-tokens.ts +14 -3
  94. package/src/shared/settings.ts +10 -2
  95. package/src/shared/shortcuts.ts +17 -0
  96. package/src/shared/types.ts +160 -10
  97. package/src/shared/utils.ts +6 -29
  98. package/src/shared/workflow-child-permit.ts +116 -0
  99. package/src/slash/delegation-adapters.ts +0 -1
  100. package/src/slash/slash-commands.ts +8 -6
  101. package/src/slash/subagents-admin.ts +3 -0
  102. package/src/tui/fleet-status.ts +27 -10
  103. package/src/tui/fleet-transcript.ts +11 -5
  104. package/src/tui/fleet.ts +28 -13
  105. package/src/tui/render.ts +55 -21
  106. package/src/workflows/scripted-workflow.ts +299 -31
  107. package/src/workflows/workflow-child-summary.ts +117 -0
  108. package/src/workflows/workflow-receipt.ts +155 -5
@@ -4,9 +4,13 @@ import * as path from "node:path";
4
4
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
5
5
  import { TEMP_ROOT_DIR, type ActiveAsyncCapacitySnapshot, type AsyncStatus } from "../../shared/types.ts";
6
6
  import { readStatus } from "../../shared/utils.ts";
7
+ import { checkPidLiveness, type PidLiveness } from "./stale-run-reconciler.ts";
7
8
  import { readProcessTerminal } from "./process-terminal.ts";
8
9
 
9
10
  export const ACTIVE_ASYNC_CAPACITY_DIR = path.join(TEMP_ROOT_DIR, "session-active-async-capacity");
11
+ export const DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS = 20 * 60 * 1000;
12
+ export const MIN_ABANDONED_SLOT_RELEASE_AFTER_MS = 5 * 60 * 1000;
13
+ export const MAX_ABANDONED_SLOT_RELEASE_AFTER_MS = 24 * 60 * 60 * 1000;
10
14
 
11
15
  export interface ActiveAsyncCapacityOwnerV1 {
12
16
  version: 1;
@@ -36,11 +40,21 @@ interface CapacityOptions {
36
40
  rootDir?: string;
37
41
  now?: () => number;
38
42
  token?: () => string;
43
+ abandonedSlotReleaseAfterMs?: number | false;
44
+ pidLiveness?: (pid: number) => PidLiveness;
39
45
  afterSlotRename?: (releasedDir: string) => void;
40
46
  }
41
47
 
48
+ export interface ActiveAsyncCapacityReleaseEvidence {
49
+ releasedBy: "abandoned-timeout";
50
+ processProof: "unknown";
51
+ runnerPid: "gone";
52
+ lastActivityAgeMs: number;
53
+ abandonedSlotReleaseAfterMs: number;
54
+ }
55
+
42
56
  export type ActiveAsyncCapacityReleaseVerdict =
43
- | { state: "releasable"; reason: string }
57
+ | { state: "releasable"; reason: string; evidence?: ActiveAsyncCapacityReleaseEvidence }
44
58
  | { state: "retained"; reason: string }
45
59
  | { state: "not-owned"; reason: string };
46
60
 
@@ -66,6 +80,12 @@ export function resolveMaxActiveAsyncRunsPerSession(value: unknown): number | un
66
80
  return value === 0 ? undefined : value;
67
81
  }
68
82
 
83
+ export function resolveAbandonedSlotReleaseAfterMs(value: unknown): number | false {
84
+ if (value === false) return false;
85
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS;
86
+ return value;
87
+ }
88
+
69
89
  export function activeAsyncCapacitySessionKey(sessionId: string): string {
70
90
  return createHash("sha256").update(sessionId).digest("hex");
71
91
  }
@@ -154,7 +174,7 @@ function withSlotClaim<T>(dir: string, operation: () => T): { acquired: true; va
154
174
  }
155
175
  }
156
176
 
157
- function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, options: CapacityOptions, requireUnstarted = false): boolean {
177
+ function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, options: CapacityOptions, requireUnstarted = false, release?: ActiveAsyncCapacityReleaseVerdict): boolean {
158
178
  if (requireUnstarted && (expected.runnerProcessInstanceId || expected.runnerStartedAt)) return false;
159
179
  const claimed = withSlotClaim(dir, () => {
160
180
  const current = matchingOwner(dir, expected);
@@ -162,17 +182,36 @@ function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, opti
162
182
  const releasedDir = path.join(path.dirname(dir), `.${path.basename(dir)}.released-${randomUUID()}`);
163
183
  fs.renameSync(dir, releasedDir);
164
184
  options.afterSlotRename?.(releasedDir);
185
+ if (release?.state === "releasable" && release.evidence) {
186
+ appendAbandonedReleaseEvent(expected.asyncDir, expected, release.evidence, options.now?.() ?? Date.now());
187
+ }
165
188
  fs.rmSync(releasedDir, { recursive: true, force: true });
166
189
  return true;
167
190
  });
168
191
  return claimed.acquired && claimed.value;
169
192
  }
170
193
 
194
+ function appendAbandonedReleaseEvent(asyncDir: string, owner: ActiveAsyncCapacityOwnerV1, evidence: ActiveAsyncCapacityReleaseEvidence, now: number): void {
195
+ try {
196
+ const eventsPath = path.join(asyncDir, "events.jsonl");
197
+ fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
198
+ fs.appendFileSync(eventsPath, `${JSON.stringify({
199
+ type: "subagent.capacity.released",
200
+ ts: now,
201
+ runId: owner.runId,
202
+ sessionId: owner.ownerSessionId,
203
+ ...evidence,
204
+ })}\n`, "utf-8");
205
+ } catch {
206
+ // Capacity release must not fail because its diagnostic event cannot be written.
207
+ }
208
+ }
209
+
171
210
  function terminalState(state: AsyncStatus["state"]): boolean {
172
211
  return state !== "queued" && state !== "running" && state !== "paused";
173
212
  }
174
213
 
175
- function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null): ActiveAsyncCapacityReleaseVerdict {
214
+ function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
176
215
  if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
177
216
  if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
178
217
  if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
@@ -191,7 +230,34 @@ function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncSt
191
230
  && proof.runId === owner.runId
192
231
  && proof.runnerProcessInstanceId === owner.runnerProcessInstanceId
193
232
  ? { state: "releasable", reason: "matching observed process-terminal proof is present" }
194
- : { state: "retained", reason: `process-terminal proof is ${proof?.state ?? "missing"}` };
233
+ : abandonedRunnerReleaseVerdict(owner, status, proof?.state ?? "missing", options);
234
+ }
235
+
236
+ function abandonedRunnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus, proofState: string, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
237
+ const proofReason = `process-terminal proof is ${proofState}`;
238
+ const thresholdMs = resolveAbandonedSlotReleaseAfterMs(options.abandonedSlotReleaseAfterMs);
239
+ if (thresholdMs === false) return { state: "retained", reason: `${proofReason}; abandoned-timeout policy is disabled` };
240
+ if (status.state !== "failed") return { state: "retained", reason: `${proofReason}; abandoned-timeout policy requires a failed run, not ${status.state}` };
241
+ if (typeof status.pid !== "number" || !Number.isInteger(status.pid) || status.pid < 1) return { state: "retained", reason: `${proofReason}; runner PID is missing or invalid` };
242
+ const liveness = (options.pidLiveness ?? checkPidLiveness)(status.pid);
243
+ if (liveness !== "dead") return { state: "retained", reason: `${proofReason}; runner PID liveness is ${liveness}` };
244
+ const lastActivityAt = status.lastActivityAt ?? status.lastUpdate ?? status.endedAt;
245
+ if (typeof lastActivityAt !== "number" || !Number.isFinite(lastActivityAt)) return { state: "retained", reason: `${proofReason}; last activity timestamp is missing or invalid` };
246
+ const now = options.now?.() ?? Date.now();
247
+ const lastActivityAgeMs = Math.max(0, now - lastActivityAt);
248
+ if (lastActivityAgeMs <= thresholdMs) return { state: "retained", reason: `${proofReason}; last activity age ${lastActivityAgeMs}ms has not exceeded abandoned-timeout ${thresholdMs}ms` };
249
+ const evidence: ActiveAsyncCapacityReleaseEvidence = {
250
+ releasedBy: "abandoned-timeout",
251
+ processProof: "unknown",
252
+ runnerPid: "gone",
253
+ lastActivityAgeMs,
254
+ abandonedSlotReleaseAfterMs: thresholdMs,
255
+ };
256
+ return {
257
+ state: "releasable",
258
+ reason: `${evidence.releasedBy}: ${proofReason}; runner PID is gone; process proof unknown; last activity age ${lastActivityAgeMs}ms exceeds ${thresholdMs}ms`,
259
+ evidence,
260
+ };
195
261
  }
196
262
 
197
263
  function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
@@ -222,10 +288,10 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
222
288
  return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
223
289
  }
224
290
 
225
- function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
291
+ function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, liveWorkflowRunIds: ReadonlySet<string>, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
226
292
  const status = readStatus(owner.asyncDir);
227
293
  return owner.kind === "runner"
228
- ? runnerReleaseVerdict(owner, status)
294
+ ? runnerReleaseVerdict(owner, status, options)
229
295
  : workflowReleaseVerdict(owner, status, liveWorkflowRunIds);
230
296
  }
231
297
 
@@ -262,7 +328,7 @@ export function inspectActiveAsyncCapacityOwner(
262
328
  release: { state: "not-owned", reason: `slot was transferred to ${owner.runId}` },
263
329
  };
264
330
  }
265
- return { owner, relation: "current", slotDir: dir, release: ownerReleaseVerdict(owner, liveWorkflowRunIds) };
331
+ return { owner, relation: "current", slotDir: dir, release: ownerReleaseVerdict(owner, liveWorkflowRunIds, options) };
266
332
  }
267
333
  }
268
334
  return { relation: "none", release: { state: "not-owned", reason: "no active-capacity slot records this run" } };
@@ -281,9 +347,10 @@ export function reconcileActiveAsyncCapacity(
281
347
  if (!owner
282
348
  || owner.ownerSessionId !== sessionId
283
349
  || owner.ownerSessionKey !== activeAsyncCapacitySessionKey(sessionId)
284
- || path.basename(dir) !== `slot-${owner.slot}`
285
- || ownerReleaseVerdict(owner, liveWorkflowRunIds).state !== "releasable") continue;
286
- removeOwnedSlot(dir, owner, options);
350
+ || path.basename(dir) !== `slot-${owner.slot}`) continue;
351
+ const release = ownerReleaseVerdict(owner, liveWorkflowRunIds, options);
352
+ if (release.state !== "releasable") continue;
353
+ removeOwnedSlot(dir, owner, options, false, release);
287
354
  }
288
355
  return snapshotFor(sessionId, limit, rootDir);
289
356
  }
@@ -10,7 +10,7 @@ import * as path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { createRequire } from "node:module";
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
- import type { AgentConfig } from "../../agents/agents.ts";
13
+ import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
14
14
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
15
15
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
16
16
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
@@ -20,6 +20,7 @@ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveC
20
20
  import type { RunnerStep } from "../shared/parallel-utils.ts";
21
21
  import type { ContextMode } from "../shared/context-mode.ts";
22
22
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
23
+ import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
23
24
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
24
25
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
25
26
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
@@ -27,7 +28,7 @@ import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } fr
27
28
  import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
28
29
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
29
30
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
30
- import { resolveEffectiveThinking } from "../../shared/model-info.ts";
31
+ import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
31
32
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
32
33
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
33
34
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
@@ -39,6 +40,7 @@ import { validateImplementationToolContract } from "../shared/completion-guard.t
39
40
  import {
40
41
  type AcceptanceInput,
41
42
  type AgentContract,
43
+ type AsyncParallelGroupStatus,
42
44
  type AsyncStatus,
43
45
  type ArtifactConfig,
44
46
  type Details,
@@ -70,6 +72,7 @@ import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
70
72
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
71
73
  import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
72
74
  import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
75
+ import { statusStepDescription } from "./chain-append.ts";
73
76
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
74
77
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
75
78
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
@@ -151,6 +154,8 @@ interface AsyncChainParams {
151
154
  attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
152
155
  resultMode?: SubagentRunMode;
153
156
  agents: AgentConfig[];
157
+ /** Original discovery provenance, retained by normal callers for unknown-agent diagnostics. */
158
+ unknownAgentDiagnosticContext?: UnknownAgentDiagnosticContext;
154
159
  ctx: AsyncExecutionContext;
155
160
  availableModels?: AvailableModelInfo[];
156
161
  cwd?: string;
@@ -208,6 +213,7 @@ interface AsyncSingleParams {
208
213
  recoveryAgentConfig?: AgentConfig;
209
214
  ctx: AsyncExecutionContext;
210
215
  cwd?: string;
216
+ requestedCwd?: string;
211
217
  maxOutput?: MaxOutputConfig;
212
218
  artifactsDir?: string;
213
219
  artifactConfig: ArtifactConfig;
@@ -283,6 +289,8 @@ export interface AsyncRunnerStepBuildParams {
283
289
  attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
284
290
  resultMode?: SubagentRunMode;
285
291
  agents: AgentConfig[];
292
+ /** Exact discovery provenance for failed resolution; omission triggers defensive fallback discovery. */
293
+ unknownAgentDiagnosticContext?: UnknownAgentDiagnosticContext;
286
294
  ctx: AsyncExecutionContext;
287
295
  availableModels?: AvailableModelInfo[];
288
296
  cwd?: string;
@@ -493,24 +501,33 @@ interface SpawnRunnerResult {
493
501
  startupDidNotProceed?: boolean;
494
502
  }
495
503
 
496
- function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): SpawnRunnerResult {
497
- if (!jitiCliPath) {
498
- return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
499
- }
504
+ function isStaleExtensionContextError(error: unknown): boolean {
505
+ return error instanceof Error && /extension ctx is stale|stale after session replacement or reload/i.test(error.message);
506
+ }
500
507
 
508
+ export function emitProcessTerminalEvent(ctx: AsyncExecutionContext, proof: unknown): void {
501
509
  try {
502
- const cwdStats = fs.statSync(cwd);
503
- if (!cwdStats.isDirectory()) {
504
- return { error: `cwd is not a directory: ${cwd}` };
505
- }
506
- } catch {
507
- return { error: `cwd does not exist: ${cwd}` };
510
+ ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof);
511
+ } catch (error) {
512
+ if (isStaleExtensionContextError(error)) return;
513
+ console.error("Failed to emit subagent process-terminal event:", error);
514
+ }
515
+ }
516
+
517
+ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, requestedCwd = cwd): SpawnRunnerResult {
518
+ const cwdError = preflightLaunchCwd(requestedCwd, cwd);
519
+ if (cwdError) return { error: cwdError };
520
+
521
+ if (!jitiCliPath) {
522
+ return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
508
523
  }
509
524
 
510
525
  fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
511
526
  const cfgPath = getAsyncConfigPath(suffix);
512
527
  const runnerProcessInstanceId = randomUUID();
513
- const launchConfig = { ...cfg, runnerProcessInstanceId };
528
+ const hasRevivalLease = typeof (cfg as { revivalLease?: unknown }).revivalLease === "object";
529
+ const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
530
+ const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
514
531
  writePrivateAtomicJson(cfgPath, launchConfig);
515
532
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
516
533
  const nodeCommand = resolveNodeExecutable();
@@ -523,7 +540,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
523
540
  ? path.join(launchAsyncDir, "runner-startup.json")
524
541
  : undefined;
525
542
  const startupAckPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-ack.json") : undefined;
526
- const startupProceedPath = startupPath ? path.join(path.dirname(startupPath), "runner-startup-proceed.json") : undefined;
543
+ const startupProceedPath = launchAsyncDir && (startupPath || launchBarrierToken)
544
+ ? path.join(launchAsyncDir, "runner-startup-proceed.json")
545
+ : undefined;
527
546
  if (startupPath) fs.rmSync(startupPath, { force: true });
528
547
  if (startupAckPath) fs.rmSync(startupAckPath, { force: true });
529
548
  if (startupProceedPath) fs.rmSync(startupProceedPath, { force: true });
@@ -605,6 +624,27 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
605
624
  if (typeof proc.pid !== "number") {
606
625
  return { error: `async runner did not produce a pid for cwd: ${cwd}` };
607
626
  }
627
+ try {
628
+ writePrivateAtomicJson(initialStatusPath, {
629
+ ...initialStatus,
630
+ pid: proc.pid,
631
+ processTerminal: { version: 1, state: "pending", runId: initialStatus.runId, runnerProcessInstanceId },
632
+ });
633
+ } catch (error) {
634
+ const message = `Failed to persist initial async status: ${error instanceof Error ? error.message : String(error)}`;
635
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
636
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
637
+ }
638
+ if (launchBarrierToken && startupProceedPath) {
639
+ try {
640
+ writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: launchBarrierToken });
641
+ } catch (error) {
642
+ const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
643
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
644
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
645
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
646
+ }
647
+ }
608
648
  proc.unref();
609
649
  if (startupPath && startupAckPath && startupProceedPath) {
610
650
  const persistStartupFailure = (message: string) => {
@@ -709,6 +749,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
709
749
  }
710
750
  const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
711
751
 
752
+ const diagnosticContext = params.unknownAgentDiagnosticContext
753
+ ?? unknownAgentDiagnosticContext(discoverAgents(path.resolve(runnerCwd), "both"));
712
754
  for (const s of chain) {
713
755
  const stepAgents = isParallelStep(s)
714
756
  ? s.parallel.map((t) => t.agent)
@@ -716,9 +758,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
716
758
  ? [s.parallel.agent]
717
759
  : [(s as SequentialStep).agent];
718
760
  for (const agentName of stepAgents) {
719
- if (!agents.find((x) => x.name === agentName)) {
720
- return { error: `Unknown agent: ${agentName}` };
721
- }
761
+ if (!agents.find((x) => x.name === agentName)) return { error: formatUnknownAgentError(agentName, diagnosticContext) };
722
762
  }
723
763
  }
724
764
 
@@ -824,6 +864,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
824
864
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
825
865
  const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
826
866
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
867
+ const contextLimit = model ? findModelInfo(model, availableModels, a.modelProvider ?? ctx.currentModelProvider)?.contextWindow : undefined;
827
868
  const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
828
869
  params.thinkingCeiling,
829
870
  a.maxThinking,
@@ -868,6 +909,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
868
909
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
869
910
  agentName: a.name,
870
911
  permissionRules,
912
+ runtimeSnapshotHost: ctx.pi,
871
913
  });
872
914
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
873
915
  if (externalRunner && permissionRules) {
@@ -902,11 +944,15 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
902
944
  outputName: s.as,
903
945
  structured: Boolean(s.outputSchema),
904
946
  cwd: stepCwd,
947
+ requestedCwd: s.cwd ?? stepCwd,
905
948
  model,
949
+ ...(contextLimit !== undefined ? { contextLimit } : {}),
906
950
  ...(fast !== undefined ? { fast } : {}),
907
951
  thinking: resolveEffectiveThinking(model, effectiveThinking),
908
952
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
909
953
  launchResolvedExtensions,
954
+ ...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
955
+ ...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
910
956
  modelCandidates: externalRunner ? undefined : modelCandidates,
911
957
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
912
958
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
@@ -914,10 +960,12 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
914
960
  extensions: a.extensions,
915
961
  subagentOnlyExtensions: a.subagentOnlyExtensions,
916
962
  mcpDirectTools: a.mcpDirectTools,
963
+ mutationTools: a.mutationTools,
917
964
  completionGuard: a.completionGuard,
918
965
  systemPrompt,
919
966
  systemPromptMode: a.systemPromptMode,
920
967
  inheritProjectContext: a.inheritProjectContext,
968
+ inheritGlobalContext: a.inheritGlobalContext,
921
969
  inheritSkills: a.inheritSkills,
922
970
  skills: resolvedSkills.map((r) => r.name),
923
971
  outputPath,
@@ -1054,6 +1102,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1054
1102
  index: params.attachRoot.index,
1055
1103
  },
1056
1104
  inheritProjectContext: false,
1105
+ inheritGlobalContext: false,
1057
1106
  inheritSkills: false,
1058
1107
  }, ...builtSteps]
1059
1108
  : builtSteps;
@@ -1140,6 +1189,7 @@ export function executeAsyncChain(
1140
1189
  attachRoot: params.attachRoot,
1141
1190
  resultMode,
1142
1191
  agents,
1192
+ unknownAgentDiagnosticContext: params.unknownAgentDiagnosticContext,
1143
1193
  ctx,
1144
1194
  availableModels: params.availableModels,
1145
1195
  cwd,
@@ -1191,6 +1241,21 @@ export function executeAsyncChain(
1191
1241
  }
1192
1242
  return "agent" in step ? [childIntercomTarget(step.agent, childTargetIndex++)] : [undefined];
1193
1243
  }) : undefined;
1244
+ const initialStatusSteps = eventChain.flatMap((step) => isParallelStep(step)
1245
+ ? step.parallel.map((task) => ({ agent: task.agent, ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}), ...(task.label ? { label: task.label } : {}), ...(task.as ? { outputName: task.as } : {}), status: "pending" as const }))
1246
+ : isDynamicParallelStep(step)
1247
+ ? [{ agent: `expand:${step.parallel.agent}`, label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`, outputName: step.collect.as, status: "pending" as const }]
1248
+ : [{ agent: (step as SequentialStep).agent, ...(statusStepDescription((step as SequentialStep).task) ? { description: statusStepDescription((step as SequentialStep).task) } : {}), ...((step as SequentialStep).label ? { label: (step as SequentialStep).label } : {}), ...((step as SequentialStep).as ? { outputName: (step as SequentialStep).as } : {}), status: "pending" as const }]);
1249
+ const initialParallelGroups: AsyncParallelGroupStatus[] = [];
1250
+ let initialFlatIndex = 0;
1251
+ for (let stepIndex = 0; stepIndex < eventChain.length; stepIndex++) {
1252
+ const step = eventChain[stepIndex]!;
1253
+ if (isParallelStep(step)) initialParallelGroups.push({ start: initialFlatIndex, count: step.parallel.length, stepIndex });
1254
+ else if (isDynamicParallelStep(step)) initialParallelGroups.push({ start: initialFlatIndex, count: 1, stepIndex });
1255
+ initialFlatIndex += isParallelStep(step) ? step.parallel.length : 1;
1256
+ }
1257
+ const initialStatusAt = Date.now();
1258
+ const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
1194
1259
 
1195
1260
  let spawnResult: SpawnRunnerResult = {};
1196
1261
  try {
@@ -1240,7 +1305,22 @@ export function executeAsyncChain(
1240
1305
  },
1241
1306
  id,
1242
1307
  runnerCwd,
1243
- (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1308
+ {
1309
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1310
+ runId: id,
1311
+ ...(ctx.currentSessionId ? { sessionId: ctx.currentSessionId } : {}),
1312
+ ...(initialCompletionOwnerId ? { completionOwnerId: initialCompletionOwnerId } : {}),
1313
+ mode: resultMode,
1314
+ state: "running",
1315
+ startedAt: initialStatusAt,
1316
+ lastUpdate: initialStatusAt,
1317
+ currentStep: 0,
1318
+ chainStepCount: eventChain.length,
1319
+ ...(initialParallelGroups.length ? { parallelGroups: initialParallelGroups } : {}),
1320
+ steps: initialStatusSteps,
1321
+ },
1322
+ path.join(asyncDir, "status.json"),
1323
+ (proof) => emitProcessTerminalEvent(ctx, proof),
1244
1324
  );
1245
1325
  } catch (error) {
1246
1326
  params.activeAsyncCapacity?.rollback();
@@ -1507,6 +1587,7 @@ export function executeAsyncSingle(
1507
1587
  );
1508
1588
  const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1509
1589
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1590
+ const contextLimit = model ? findModelInfo(model, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider)?.contextWindow : undefined;
1510
1591
  const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
1511
1592
  params.thinkingCeiling,
1512
1593
  agentConfig.maxThinking,
@@ -1574,6 +1655,7 @@ export function executeAsyncSingle(
1574
1655
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1575
1656
  agentName: agentConfig.name,
1576
1657
  permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1658
+ runtimeSnapshotHost: ctx.pi,
1577
1659
  });
1578
1660
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1579
1661
  if (!externalRunner) {
@@ -1601,6 +1683,7 @@ export function executeAsyncSingle(
1601
1683
  systemPrompt: effectiveSystemPrompt,
1602
1684
  systemPromptMode: agentConfig.systemPromptMode,
1603
1685
  inheritProjectContext: agentConfig.inheritProjectContext,
1686
+ inheritGlobalContext: agentConfig.inheritGlobalContext,
1604
1687
  inheritSkills: agentConfig.inheritSkills,
1605
1688
  skills: resolvedSkills.map((skill) => skill.name),
1606
1689
  tools: toolPlan.effectiveToolAllowlist,
@@ -1643,9 +1726,11 @@ export function executeAsyncSingle(
1643
1726
  ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1644
1727
  ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
1645
1728
  ...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
1729
+ ...(recoveryAgentConfig.mutationTools ? { mutationTools: [...recoveryAgentConfig.mutationTools] } : {}),
1646
1730
  ...(recoveryAgentConfig.systemPrompt ? { systemPrompt: recoveryAgentConfig.systemPrompt } : {}),
1647
1731
  systemPromptMode: recoveryAgentConfig.systemPromptMode,
1648
1732
  inheritProjectContext: recoveryAgentConfig.inheritProjectContext,
1733
+ inheritGlobalContext: recoveryAgentConfig.inheritGlobalContext,
1649
1734
  inheritSkills: recoveryAgentConfig.inheritSkills,
1650
1735
  ...(resolvedSkills.length ? { skills: resolvedSkills.map((skill) => skill.name) } : {}),
1651
1736
  ...(recoveryAgentConfig.skillPath ? { skillPath: [...recoveryAgentConfig.skillPath] } : {}),
@@ -1678,6 +1763,8 @@ export function executeAsyncSingle(
1678
1763
  }
1679
1764
  }
1680
1765
  let spawnResult: SpawnRunnerResult = {};
1766
+ const initialStatusAt = Date.now();
1767
+ const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
1681
1768
  try {
1682
1769
  spawnResult = spawnRunner(
1683
1770
  {
@@ -1693,7 +1780,9 @@ export function executeAsyncSingle(
1693
1780
  ...(params.externalJobFollowUp ? { externalJobFollowUp: params.externalJobFollowUp } : {}),
1694
1781
  ...(params.context ? { context: params.context } : {}),
1695
1782
  cwd: runnerCwd,
1783
+ requestedCwd: params.requestedCwd ?? runnerCwd,
1696
1784
  model,
1785
+ ...(contextLimit !== undefined ? { contextLimit } : {}),
1697
1786
  ...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
1698
1787
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1699
1788
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
@@ -1704,10 +1793,14 @@ export function executeAsyncSingle(
1704
1793
  extensions: agentConfig.extensions,
1705
1794
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1706
1795
  mcpDirectTools: agentConfig.mcpDirectTools,
1796
+ ...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
1797
+ ...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
1798
+ mutationTools: agentConfig.mutationTools,
1707
1799
  completionGuard: agentConfig.completionGuard,
1708
1800
  systemPrompt,
1709
1801
  systemPromptMode: agentConfig.systemPromptMode,
1710
1802
  inheritProjectContext: agentConfig.inheritProjectContext,
1803
+ inheritGlobalContext: agentConfig.inheritGlobalContext,
1711
1804
  inheritSkills: agentConfig.inheritSkills,
1712
1805
  skills: resolvedSkills.map((r) => r.name),
1713
1806
  outputPath,
@@ -1773,7 +1866,22 @@ export function executeAsyncSingle(
1773
1866
  },
1774
1867
  id,
1775
1868
  runnerCwd,
1776
- (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1869
+ {
1870
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1871
+ runId: id,
1872
+ ...(ctx.currentSessionId ? { sessionId: ctx.currentSessionId } : {}),
1873
+ ...(initialCompletionOwnerId ? { completionOwnerId: initialCompletionOwnerId } : {}),
1874
+ mode: "single",
1875
+ state: "running",
1876
+ startedAt: initialStatusAt,
1877
+ lastUpdate: initialStatusAt,
1878
+ currentStep: 0,
1879
+ chainStepCount: 1,
1880
+ steps: [{ agent, status: "pending", ...(model ? { model } : {}), ...(contextLimit !== undefined ? { contextLimit } : {}) }],
1881
+ },
1882
+ path.join(asyncDir, "status.json"),
1883
+ (proof) => emitProcessTerminalEvent(ctx, proof),
1884
+ params.requestedCwd ?? runnerCwd,
1777
1885
  );
1778
1886
  } catch (error) {
1779
1887
  params.activeAsyncCapacity?.rollback();
@@ -24,6 +24,7 @@ import { findNestedRouteForRootId, hasLiveNestedDescendants, updateAsyncJobNeste
24
24
  import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
25
25
  import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } from "../shared/external-job-bridge.ts";
26
26
  import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
27
+ import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
27
28
 
28
29
  interface AsyncJobTrackerOptions {
29
30
  completionRetentionMs?: number;
@@ -171,6 +172,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
171
172
  parentWorkflowRunId: run.parentWorkflowRunId,
172
173
  workflowKey: run.workflowKey,
173
174
  workflow: run.workflow,
175
+ workflowChildren: parseWorkflowChildSummary(run.workflowChildren),
174
176
  };
175
177
  };
176
178
  const cancelCleanup = (asyncId: string) => {
@@ -398,6 +400,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
398
400
  job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
399
401
  job.workflowKey = status.workflowKey ?? job.workflowKey;
400
402
  job.workflow = status.workflow ?? job.workflow;
403
+ const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
404
+ if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
405
+ job.workflowChildren = workflowChildren ?? job.workflowChildren;
401
406
  job.currentStep = status.currentStep ?? job.currentStep;
402
407
  job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
403
408
  job.startedAt = status.startedAt ?? job.startedAt;
@@ -325,7 +325,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
325
325
  const parsed = value as Record<string, unknown>;
326
326
  const allowedFields = new Set([
327
327
  "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "extensions",
328
- "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
328
+ "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
329
329
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
330
330
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
331
331
  "launchResolvedExtensions", "runFanoutBudget",
@@ -359,8 +359,10 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
359
359
  for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
360
360
  if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
361
361
  }
362
+ if (parsed.inheritGlobalContext === undefined) parsed.inheritGlobalContext = parsed.inheritProjectContext;
363
+ else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
362
364
  if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
363
- for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "skills", "skillPath"] as const) {
365
+ for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
364
366
  const item = parsed[field];
365
367
  if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
366
368
  }
@@ -592,9 +594,11 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
592
594
  extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
593
595
  subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
594
596
  mcpDirectTools: descriptor.mcpDirectTools ? [...descriptor.mcpDirectTools] : undefined,
597
+ mutationTools: descriptor.mutationTools ? [...descriptor.mutationTools] : undefined,
595
598
  systemPrompt: descriptor.systemPrompt ?? agentConfig.systemPrompt,
596
599
  systemPromptMode: descriptor.systemPromptMode,
597
600
  inheritProjectContext: descriptor.inheritProjectContext,
601
+ inheritGlobalContext: descriptor.inheritGlobalContext,
598
602
  inheritSkills: descriptor.inheritSkills,
599
603
  skills: descriptor.skills ? [...descriptor.skills] : undefined,
600
604
  skillPath: descriptor.skillPath ? [...descriptor.skillPath] : undefined,