pi-subagents 0.29.0 → 0.31.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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +125 -19
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +30 -0
  9. package/src/agents/agent-management.ts +189 -8
  10. package/src/agents/agent-serializer.ts +35 -12
  11. package/src/agents/agents.ts +243 -24
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/proactive-skills.ts +191 -0
  14. package/src/agents/skills.ts +117 -20
  15. package/src/extension/doctor.ts +20 -0
  16. package/src/extension/fanout-child.ts +2 -1
  17. package/src/extension/index.ts +50 -5
  18. package/src/extension/schemas.ts +40 -79
  19. package/src/intercom/intercom-bridge.ts +2 -3
  20. package/src/runs/background/async-execution.ts +180 -67
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +53 -5
  23. package/src/runs/background/async-status.ts +4 -1
  24. package/src/runs/background/chain-append.ts +282 -0
  25. package/src/runs/background/chain-root-attachment.ts +161 -0
  26. package/src/runs/background/result-watcher.ts +11 -2
  27. package/src/runs/background/run-status.ts +1 -0
  28. package/src/runs/background/stale-run-reconciler.ts +9 -4
  29. package/src/runs/background/subagent-runner.ts +158 -11
  30. package/src/runs/foreground/chain-execution.ts +26 -2
  31. package/src/runs/foreground/execution.ts +114 -8
  32. package/src/runs/foreground/subagent-executor.ts +611 -87
  33. package/src/runs/shared/acceptance.ts +285 -34
  34. package/src/runs/shared/chain-outputs.ts +23 -8
  35. package/src/runs/shared/completion-guard.ts +1 -1
  36. package/src/runs/shared/dynamic-fanout.ts +5 -3
  37. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  38. package/src/runs/shared/parallel-utils.ts +13 -1
  39. package/src/runs/shared/pi-args.ts +12 -3
  40. package/src/runs/shared/single-output.ts +15 -1
  41. package/src/runs/shared/subagent-control.ts +8 -11
  42. package/src/shared/settings.ts +1 -0
  43. package/src/shared/types.ts +17 -2
  44. package/src/shared/utils.ts +19 -1
  45. package/src/slash/prompt-template-bridge.ts +26 -3
  46. package/src/slash/slash-bridge.ts +3 -1
  47. package/src/slash/slash-commands.ts +34 -4
  48. package/src/tui/render.ts +265 -13
@@ -5,13 +5,38 @@
5
5
  import { Type } from "typebox";
6
6
  import { SUBAGENT_ACTIONS } from "../shared/types.ts";
7
7
 
8
+ function keepTopLevelParameterDescriptions<T>(schema: T): T {
9
+ return pruneNestedDescriptions(schema, []) as T;
10
+ }
11
+
12
+ function pruneNestedDescriptions(value: unknown, path: string[]): unknown {
13
+ if (!value || typeof value !== "object") return value;
14
+
15
+ const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
16
+ for (const key of Reflect.ownKeys(value)) {
17
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
18
+ if (!descriptor) continue;
19
+ if (key === "description" && !isTopLevelParameterDescription(path)) continue;
20
+ if ("value" in descriptor) {
21
+ const nextPath = typeof key === "string" ? [...path, key] : path;
22
+ descriptor.value = pruneNestedDescriptions(descriptor.value, nextPath);
23
+ }
24
+ Object.defineProperty(result, key, descriptor);
25
+ }
26
+ return result;
27
+ }
28
+
29
+ function isTopLevelParameterDescription(path: string[]): boolean {
30
+ return path.length === 2 && path[0] === "properties";
31
+ }
32
+
8
33
  const SkillOverride = Type.Unsafe({
9
34
  anyOf: [
10
35
  { type: "array", items: { type: "string" } },
11
36
  { type: "boolean" },
12
37
  { type: "string" },
13
38
  ],
14
- description: "Skill name(s) to inject (comma-separated), array of strings, or boolean (false disables, true uses default)",
39
+ description: "Skill name(s) to make available (comma-separated), array of strings, or boolean (false disables, true uses default)",
15
40
  });
16
41
 
17
42
  const OutputOverride = Type.Unsafe({
@@ -41,72 +66,11 @@ const JsonSchemaObject = Type.Unsafe({
41
66
  description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
42
67
  });
43
68
 
44
- const AcceptanceEvidenceKind = Type.String({
45
- enum: [
46
- "changed-files",
47
- "tests-added",
48
- "commands-run",
49
- "validation-output",
50
- "residual-risks",
51
- "no-staged-files",
52
- "diff-summary",
53
- "review-findings",
54
- "manual-notes",
55
- ],
56
- });
57
-
58
- const AcceptanceGateSchema = Type.Object({
59
- id: Type.String(),
60
- must: Type.String(),
61
- evidence: Type.Optional(Type.Array(AcceptanceEvidenceKind)),
62
- severity: Type.Optional(Type.String({ enum: ["required", "recommended"] })),
63
- }, { additionalProperties: false });
64
-
65
- const AcceptanceVerifyCommandSchema = Type.Object({
66
- id: Type.String(),
67
- command: Type.String(),
68
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
69
- cwd: Type.Optional(Type.String()),
70
- env: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: { type: "string" } })),
71
- allowFailure: Type.Optional(Type.Boolean()),
72
- }, { additionalProperties: false });
73
-
74
- const AcceptanceReviewGateSchema = Type.Object({
75
- agent: Type.Optional(Type.String()),
76
- focus: Type.Optional(Type.String()),
77
- required: Type.Optional(Type.Boolean()),
78
- }, { additionalProperties: false });
79
-
80
69
  const AcceptanceOverride = Type.Unsafe({
81
70
  anyOf: [
82
71
  { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
83
- { const: false },
84
- {
85
- type: "object",
86
- properties: {
87
- level: { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
88
- criteria: {
89
- type: "array",
90
- items: {
91
- anyOf: [
92
- { type: "string" },
93
- AcceptanceGateSchema,
94
- ],
95
- },
96
- },
97
- evidence: { type: "array", items: AcceptanceEvidenceKind },
98
- verify: { type: "array", items: AcceptanceVerifyCommandSchema },
99
- review: {
100
- anyOf: [
101
- { const: false },
102
- AcceptanceReviewGateSchema,
103
- ],
104
- },
105
- stopRules: { type: "array", items: { type: "string" } },
106
- reason: { type: "string" },
107
- },
108
- additionalProperties: false,
109
- },
72
+ { type: "boolean", enum: [false] },
73
+ { type: "object", additionalProperties: true },
110
74
  ],
111
75
  description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
112
76
  });
@@ -211,11 +175,6 @@ const ChainItem = Type.Object({
211
175
  }, {
212
176
  description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
213
177
  additionalProperties: false,
214
- allOf: [
215
- { if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
216
- { if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
217
- { not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
218
- ],
219
178
  });
220
179
 
221
180
  const ControlOverrides = Type.Object({
@@ -233,7 +192,7 @@ const ControlOverrides = Type.Object({
233
192
  })),
234
193
  });
235
194
 
236
- export const SubagentParams = Type.Object({
195
+ const SubagentParamsSchema = Type.Object({
237
196
  agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
238
197
  task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
239
198
  // Management action (when present, tool operates in management mode)
@@ -242,10 +201,10 @@ export const SubagentParams = Type.Object({
242
201
  description: "Management/control action. Omit for execution mode."
243
202
  })),
244
203
  id: Type.Optional(Type.String({
245
- description: "Run id or prefix for action='status', action='interrupt', or action='resume'."
204
+ description: "Run id or prefix for action='status', action='interrupt', action='resume', or action='append-step'."
246
205
  })),
247
206
  runId: Type.Optional(Type.String({
248
- description: "Target run ID for action='interrupt' or action='resume'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
207
+ description: "Target run ID for action='interrupt', action='resume', or action='append-step'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
249
208
  })),
250
209
  dir: Type.Optional(Type.String({
251
210
  description: "Async run directory for action='status' or action='resume'."
@@ -262,22 +221,22 @@ export const SubagentParams = Type.Object({
262
221
  { type: "object", additionalProperties: true },
263
222
  { type: "string" },
264
223
  ],
265
- description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
224
+ description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
266
225
  })),
267
226
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
268
227
  concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
269
228
  worktree: Type.Optional(Type.Boolean({
270
- description: "Create isolated git worktrees for each parallel task. " +
271
- "Prevents filesystem conflicts. Requires clean git state. " +
272
- "Per-worktree diffs included in output."
229
+ description: "Create isolated git worktrees for parallel tasks; requires clean git state."
273
230
  })),
274
- chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
231
+ chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential steps; each result becomes {previous}. append-step takes one tail step and may use {chain_dir}/{outputs.name}." })),
275
232
  context: Type.Optional(Type.String({
276
233
  enum: ["fresh", "fork"],
277
- description: "'fresh' or 'fork' to branch from parent session. If omitted, any requested agent with defaultContext: 'fork' makes the whole invocation forked; otherwise the default is 'fresh'.",
234
+ description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
278
235
  })),
279
- chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: a user-scoped temp directory under <tmpdir>/ (auto-cleaned after 24h)" })),
236
+ chainDir: Type.Optional(Type.String({ description: "Persistent chain artifact directory; defaults to user-scoped temp storage." })),
280
237
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
238
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Foreground timeout ms; alias of maxRuntimeMs." })),
239
+ maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias of timeoutMs for foreground timeout." })),
281
240
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
282
241
  cwd: Type.Optional(Type.String()),
283
242
  artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
@@ -302,3 +261,5 @@ export const SubagentParams = Type.Object({
302
261
  model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
303
262
  acceptance: Type.Optional(AcceptanceOverride),
304
263
  });
264
+
265
+ export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
@@ -4,10 +4,9 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import type { AgentConfig } from "../agents/agents.ts";
6
6
  import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
7
- import { getAgentDir } from "../shared/utils.ts";
7
+ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
8
8
 
9
9
  const PI_INTERCOM_PACKAGE_NAME = "pi-intercom";
10
- const CONFIG_DIR = ".pi";
11
10
 
12
11
  function defaultAgentDir(): string {
13
12
  return getAgentDir();
@@ -179,7 +178,7 @@ function packageEntryAllowsExtensions(entry: unknown): boolean {
179
178
  function findNearestProjectConfigDir(cwd: string): string | undefined {
180
179
  let current = path.resolve(cwd);
181
180
  while (true) {
182
- const configDir = path.join(current, CONFIG_DIR);
181
+ const configDir = getProjectConfigDir(current);
183
182
  if (fs.existsSync(path.join(configDir, "settings.json"))) return configDir;
184
183
  const parent = path.dirname(current);
185
184
  if (parent === current) return undefined;
@@ -11,7 +11,7 @@ import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import type { AgentConfig } from "../../agents/agents.ts";
13
13
  import { applyThinkingSuffix } from "../shared/pi-args.ts";
14
- import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
14
+ import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
15
15
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
16
16
  import type { RunnerStep } from "../shared/parallel-utils.ts";
17
17
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
@@ -40,6 +40,7 @@ import {
40
40
  resolveChildMaxSubagentDepth,
41
41
  } from "../../shared/types.ts";
42
42
  import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
43
+ import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
43
44
 
44
45
  const require = createRequire(import.meta.url);
45
46
  const piPackageRoot = resolvePiPackageRoot();
@@ -94,6 +95,8 @@ interface AsyncExecutionContext {
94
95
  pi: ExtensionAPI;
95
96
  cwd: string;
96
97
  currentSessionId: string;
98
+ /** Parent session id used by permission-system ask forwarding. */
99
+ parentSessionId?: string;
97
100
  currentModelProvider?: string;
98
101
  currentModel?: ParentModel;
99
102
  }
@@ -101,6 +104,7 @@ interface AsyncExecutionContext {
101
104
  interface AsyncChainParams {
102
105
  chain: ChainStep[];
103
106
  task?: string;
107
+ attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
104
108
  resultMode?: Exclude<SubagentRunMode, "single">;
105
109
  agents: AgentConfig[];
106
110
  ctx: AsyncExecutionContext;
@@ -113,6 +117,7 @@ interface AsyncChainParams {
113
117
  sessionRoot?: string;
114
118
  chainSkills?: string[];
115
119
  sessionFilesByFlatIndex?: (string | undefined)[];
120
+ progressDir?: string;
116
121
  dynamicFanoutMaxItems?: number;
117
122
  maxSubagentDepth: number;
118
123
  worktreeSetupHook?: string;
@@ -157,6 +162,34 @@ interface AsyncExecutionResult {
157
162
  isError?: boolean;
158
163
  }
159
164
 
165
+ export interface AsyncRunnerStepBuildParams {
166
+ chain: ChainStep[];
167
+ task?: string;
168
+ attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
169
+ resultMode?: SubagentRunMode;
170
+ agents: AgentConfig[];
171
+ ctx: AsyncExecutionContext;
172
+ availableModels?: AvailableModelInfo[];
173
+ cwd?: string;
174
+ chainSkills?: string[];
175
+ sessionFilesByFlatIndex?: (string | undefined)[];
176
+ progressDir?: string;
177
+ dynamicFanoutMaxItems?: number;
178
+ maxSubagentDepth: number;
179
+ asyncDir: string;
180
+ validateOutputBindings?: boolean;
181
+ }
182
+
183
+ export type AsyncRunnerStepBuildResult =
184
+ | {
185
+ steps: RunnerStep[];
186
+ runnerCwd: string;
187
+ workflowGraph: ReturnType<typeof buildWorkflowGraphSnapshot>;
188
+ eventChain: ChainStep[];
189
+ originalTask?: string;
190
+ }
191
+ | { error: string };
192
+
160
193
  export function formatAsyncStartedMessage(headline: string): string {
161
194
  return [
162
195
  headline,
@@ -174,6 +207,14 @@ export function isAsyncAvailable(): boolean {
174
207
  return jitiCliPath !== undefined;
175
208
  }
176
209
 
210
+ function resolveAsyncRunnerNodeCommand(): string {
211
+ const basename = path.basename(process.execPath).toLowerCase();
212
+ if (basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe") {
213
+ return process.execPath;
214
+ }
215
+ return process.platform === "win32" ? "node.exe" : "node";
216
+ }
217
+
177
218
  /**
178
219
  * Spawn the async runner process
179
220
  */
@@ -195,8 +236,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
195
236
  const cfgPath = getAsyncConfigPath(suffix);
196
237
  fs.writeFileSync(cfgPath, JSON.stringify(cfg));
197
238
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
239
+ const nodeCommand = resolveAsyncRunnerNodeCommand();
198
240
 
199
- const proc = spawn(process.execPath, [jitiCliPath, runner, cfgPath], {
241
+ const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
200
242
  cwd,
201
243
  detached: true,
202
244
  stdio: "ignore",
@@ -225,36 +267,29 @@ const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
225
267
  class UnavailableSubagentSkillError extends Error {}
226
268
  class AsyncStartValidationError extends Error {}
227
269
 
228
- /**
229
- * Execute a chain asynchronously
230
- */
231
- export function executeAsyncChain(
232
- id: string,
233
- params: AsyncChainParams,
234
- ): AsyncExecutionResult {
270
+ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildParams): AsyncRunnerStepBuildResult {
235
271
  const {
236
272
  chain,
237
273
  agents,
238
274
  ctx,
239
275
  cwd,
240
- maxOutput,
241
- artifactsDir,
242
- artifactConfig,
243
- shareEnabled,
244
- sessionRoot,
245
276
  sessionFilesByFlatIndex,
246
277
  maxSubagentDepth,
247
- worktreeSetupHook,
248
- worktreeSetupHookTimeoutMs,
249
- controlConfig,
250
- controlIntercomTarget,
251
- childIntercomTarget,
252
- nestedRoute,
278
+ asyncDir,
253
279
  } = params;
254
280
  const resultMode = params.resultMode ?? "chain";
255
281
  const chainSkills = params.chainSkills ?? [];
256
282
  const availableModels = params.availableModels;
257
283
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
284
+ const progressDir = params.progressDir ?? runnerCwd;
285
+ const graphChain: ChainStep[] = params.attachRoot
286
+ ? [{
287
+ agent: params.attachRoot.agent,
288
+ task: `Attach async root ${params.attachRoot.runId}`,
289
+ label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
290
+ ...(params.attachRoot.outputName ? { as: params.attachRoot.outputName } : {}),
291
+ }, ...chain]
292
+ : chain;
258
293
  const firstStep = chain[0];
259
294
  const originalTask = params.task ?? (firstStep
260
295
  ? (isParallelStep(firstStep)
@@ -264,46 +299,28 @@ export function executeAsyncChain(
264
299
  : (firstStep as SequentialStep).task)
265
300
  : undefined);
266
301
  try {
267
- validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
302
+ if (params.validateOutputBindings !== false) {
303
+ validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
304
+ }
268
305
  } catch (error) {
269
- if (error instanceof ChainOutputValidationError) return formatAsyncStartError(resultMode, error.message);
306
+ if (error instanceof ChainOutputValidationError) return { error: error.message };
270
307
  throw error;
271
308
  }
272
- const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: chain });
309
+ const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
273
310
 
274
311
  for (const s of chain) {
275
312
  const stepAgents = isParallelStep(s)
276
313
  ? s.parallel.map((t) => t.agent)
277
314
  : isDynamicParallelStep(s)
278
315
  ? [s.parallel.agent]
279
- : [(s as SequentialStep).agent];
316
+ : [(s as SequentialStep).agent];
280
317
  for (const agentName of stepAgents) {
281
318
  if (!agents.find((x) => x.name === agentName)) {
282
- return {
283
- content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
284
- isError: true,
285
- details: { mode: resultMode, results: [] },
286
- };
319
+ return { error: `Unknown agent: ${agentName}` };
287
320
  }
288
321
  }
289
322
  }
290
323
 
291
- const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
292
- const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
293
- const asyncDir = inheritedNestedRoute
294
- ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
295
- : path.join(ASYNC_DIR, id);
296
- try {
297
- fs.mkdirSync(asyncDir, { recursive: true });
298
- } catch (error) {
299
- const message = error instanceof Error ? error.message : String(error);
300
- return {
301
- content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
302
- isError: true,
303
- details: { mode: resultMode, results: [] },
304
- };
305
- }
306
-
307
324
  let progressInstructionCreated = false;
308
325
  const buildStepOverrides = (s: SequentialStep): StepOverrides => {
309
326
  const stepSkillInput = normalizeSkillInput(s.skill);
@@ -334,8 +351,9 @@ export function executeAsyncChain(
334
351
  const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false);
335
352
  const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
336
353
  if (behavior.progress) progressInstructionCreated = true;
337
- const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, runnerCwd, isFirstProgressAgent);
354
+ const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
338
355
  const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
356
+ systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath);
339
357
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
340
358
  if (validationError) throw new AsyncStartValidationError(validationError);
341
359
  let taskTemplate = s.task ?? "{previous}";
@@ -347,6 +365,7 @@ export function executeAsyncChain(
347
365
  const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
348
366
  const model = applyThinkingSuffix(primaryModel, a.thinking);
349
367
  return {
368
+ parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
350
369
  agent: s.agent,
351
370
  task,
352
371
  phase: s.phase,
@@ -361,6 +380,7 @@ export function executeAsyncChain(
361
380
  ),
362
381
  tools: a.tools,
363
382
  extensions: a.extensions,
383
+ subagentOnlyExtensions: a.subagentOnlyExtensions,
364
384
  mcpDirectTools: a.mcpDirectTools,
365
385
  completionGuard: a.completionGuard,
366
386
  systemPrompt,
@@ -392,9 +412,8 @@ export function executeAsyncChain(
392
412
  return sessionFile;
393
413
  };
394
414
 
395
- let steps: RunnerStep[];
396
415
  try {
397
- steps = chain.map((s, stepIndex) => {
416
+ const builtSteps = chain.map((s, stepIndex) => {
398
417
  if (isParallelStep(s)) {
399
418
  const parallelBehaviors = s.parallel.map((task) => {
400
419
  const agent = agents.find((candidate) => candidate.name === task.agent)!;
@@ -402,7 +421,7 @@ export function executeAsyncChain(
402
421
  });
403
422
  const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
404
423
  if (progressPrecreated) {
405
- if (!s.worktree) writeInitialProgressFile(runnerCwd);
424
+ if (!s.worktree || params.progressDir) writeInitialProgressFile(progressDir);
406
425
  progressInstructionCreated = true;
407
426
  }
408
427
  return {
@@ -427,7 +446,7 @@ export function executeAsyncChain(
427
446
  const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agent, buildStepOverrides(s.parallel), chainSkills), s.parallel.task, originalTask);
428
447
  const progressPrecreated = behavior.progress;
429
448
  if (progressPrecreated) {
430
- writeInitialProgressFile(runnerCwd);
449
+ writeInitialProgressFile(progressDir);
431
450
  progressInstructionCreated = true;
432
451
  }
433
452
  return {
@@ -450,12 +469,103 @@ export function executeAsyncChain(
450
469
  }
451
470
  return buildSeqStep(s as SequentialStep, nextSessionFile());
452
471
  });
472
+ const steps = params.attachRoot
473
+ ? [{
474
+ agent: params.attachRoot.agent,
475
+ task: "",
476
+ label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
477
+ outputName: params.attachRoot.outputName,
478
+ importAsyncRoot: {
479
+ runId: params.attachRoot.runId,
480
+ asyncDir: params.attachRoot.asyncDir,
481
+ resultPath: params.attachRoot.resultPath,
482
+ index: params.attachRoot.index,
483
+ },
484
+ inheritProjectContext: false,
485
+ inheritSkills: false,
486
+ }, ...builtSteps]
487
+ : builtSteps;
488
+ return { steps, runnerCwd, workflowGraph, eventChain: graphChain, ...(originalTask !== undefined ? { originalTask } : {}) };
453
489
  } catch (error) {
454
- if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError(resultMode, error.message);
490
+ if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return { error: error.message };
455
491
  throw error;
456
492
  }
493
+ }
494
+
495
+ /**
496
+ * Execute a chain asynchronously
497
+ */
498
+ export function executeAsyncChain(
499
+ id: string,
500
+ params: AsyncChainParams,
501
+ ): AsyncExecutionResult {
502
+ const {
503
+ chain,
504
+ agents,
505
+ ctx,
506
+ cwd,
507
+ maxOutput,
508
+ artifactsDir,
509
+ artifactConfig,
510
+ shareEnabled,
511
+ sessionRoot,
512
+ sessionFilesByFlatIndex,
513
+ maxSubagentDepth,
514
+ worktreeSetupHook,
515
+ worktreeSetupHookTimeoutMs,
516
+ controlConfig,
517
+ controlIntercomTarget,
518
+ childIntercomTarget,
519
+ nestedRoute,
520
+ } = params;
521
+ const resultMode = params.resultMode ?? "chain";
522
+ const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
523
+ const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
524
+ const asyncDir = inheritedNestedRoute
525
+ ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
526
+ : path.join(ASYNC_DIR, id);
527
+ try {
528
+ fs.mkdirSync(asyncDir, { recursive: true });
529
+ } catch (error) {
530
+ const message = error instanceof Error ? error.message : String(error);
531
+ return {
532
+ content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
533
+ isError: true,
534
+ details: { mode: resultMode, results: [] },
535
+ };
536
+ }
537
+
538
+ const built = buildAsyncRunnerSteps(id, {
539
+ chain,
540
+ task: params.task,
541
+ attachRoot: params.attachRoot,
542
+ resultMode,
543
+ agents,
544
+ ctx,
545
+ availableModels: params.availableModels,
546
+ cwd,
547
+ chainSkills: params.chainSkills,
548
+ sessionFilesByFlatIndex,
549
+ progressDir: params.progressDir ?? (resultMode === "parallel" ? path.join(asyncDir, "progress") : undefined),
550
+ dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
551
+ maxSubagentDepth,
552
+ asyncDir,
553
+ });
554
+ if ("error" in built) {
555
+ try {
556
+ fs.rmSync(asyncDir, { recursive: true, force: true });
557
+ } catch {
558
+ // Best-effort cleanup for validation failures before the runner is spawned.
559
+ }
560
+ return formatAsyncStartError(resultMode, built.error);
561
+ }
562
+ const { steps, runnerCwd, workflowGraph, eventChain } = built;
457
563
  let childTargetIndex = 0;
458
564
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
565
+ if (!("parallel" in step) && step.importAsyncRoot) {
566
+ childTargetIndex++;
567
+ return [undefined];
568
+ }
459
569
  if ("parallel" in step) {
460
570
  if (!Array.isArray(step.parallel)) {
461
571
  childTargetIndex++;
@@ -513,17 +623,17 @@ export function executeAsyncChain(
513
623
  }
514
624
 
515
625
  if (spawnResult.pid) {
516
- const firstStep = chain[0];
517
- const firstAgents = isParallelStep(firstStep)
518
- ? firstStep.parallel.map((t) => t.agent)
519
- : isDynamicParallelStep(firstStep)
520
- ? [firstStep.parallel.agent]
521
- : [(firstStep as SequentialStep).agent];
626
+ const eventFirstStep = eventChain[0];
627
+ const firstAgents = isParallelStep(eventFirstStep)
628
+ ? eventFirstStep.parallel.map((t) => t.agent)
629
+ : isDynamicParallelStep(eventFirstStep)
630
+ ? [eventFirstStep.parallel.agent]
631
+ : [(eventFirstStep as SequentialStep).agent];
522
632
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
523
633
  const flatAgents: string[] = [];
524
634
  let flatStepStart = 0;
525
- for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
526
- const step = chain[stepIndex]!;
635
+ for (let stepIndex = 0; stepIndex < eventChain.length; stepIndex++) {
636
+ const step = eventChain[stepIndex]!;
527
637
  if (isParallelStep(step)) {
528
638
  parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
529
639
  flatAgents.push(...step.parallel.map((task) => task.agent));
@@ -561,7 +671,7 @@ export function executeAsyncChain(
561
671
  state: "running",
562
672
  agent: firstAgents[0],
563
673
  agents: flatAgents,
564
- chainStepCount: chain.length,
674
+ chainStepCount: eventChain.length,
565
675
  parallelGroups,
566
676
  startedAt: now,
567
677
  lastUpdate: now,
@@ -578,15 +688,15 @@ export function executeAsyncChain(
578
688
  mode: resultMode,
579
689
  agent: firstAgents[0],
580
690
  agents: flatAgents,
581
- task: isParallelStep(firstStep)
582
- ? firstStep.parallel[0]?.task?.slice(0, 50)
583
- : isDynamicParallelStep(firstStep)
584
- ? firstStep.parallel.task?.slice(0, 50)
585
- : (firstStep as SequentialStep).task?.slice(0, 50),
586
- chain: chain.map((s) =>
691
+ task: isParallelStep(eventFirstStep)
692
+ ? eventFirstStep.parallel[0]?.task?.slice(0, 50)
693
+ : isDynamicParallelStep(eventFirstStep)
694
+ ? eventFirstStep.parallel.task?.slice(0, 50)
695
+ : (eventFirstStep as SequentialStep).task?.slice(0, 50),
696
+ chain: eventChain.map((s) =>
587
697
  isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
588
698
  ),
589
- chainStepCount: chain.length,
699
+ chainStepCount: eventChain.length,
590
700
  parallelGroups,
591
701
  workflowGraph,
592
702
  cwd: runnerCwd,
@@ -663,6 +773,7 @@ export function executeAsyncSingle(
663
773
 
664
774
  const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
665
775
  const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd);
776
+ systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath);
666
777
  const outputMode = params.outputMode ?? "inline";
667
778
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
668
779
  if (validationError) return formatAsyncStartError("single", validationError);
@@ -681,6 +792,7 @@ export function executeAsyncSingle(
681
792
  id,
682
793
  steps: [
683
794
  {
795
+ parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
684
796
  agent,
685
797
  task: taskWithOutputInstruction,
686
798
  cwd: runnerCwd,
@@ -691,6 +803,7 @@ export function executeAsyncSingle(
691
803
  ),
692
804
  tools: agentConfig.tools,
693
805
  extensions: agentConfig.extensions,
806
+ subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
694
807
  mcpDirectTools: agentConfig.mcpDirectTools,
695
808
  completionGuard: agentConfig.completionGuard,
696
809
  systemPrompt,