pi-subagents 0.14.0 → 0.15.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.
@@ -16,6 +16,7 @@ import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialSte
16
16
  import type { RunnerStep } from "./parallel-utils.ts";
17
17
  import { resolvePiPackageRoot } from "./pi-spawn.ts";
18
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "./skills.ts";
19
+ import { resolveChildCwd } from "./utils.ts";
19
20
  import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "./model-fallback.ts";
20
21
  import {
21
22
  type ArtifactConfig,
@@ -163,6 +164,7 @@ export function executeAsyncChain(
163
164
  } = params;
164
165
  const chainSkills = params.chainSkills ?? [];
165
166
  const availableModels = params.availableModels;
167
+ const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
166
168
 
167
169
  for (const s of chain) {
168
170
  const stepAgents = isParallelStep(s)
@@ -193,27 +195,27 @@ export function executeAsyncChain(
193
195
 
194
196
  const buildSeqStep = (s: SequentialStep, sessionFile?: string) => {
195
197
  const a = agents.find((x) => x.name === s.agent)!;
198
+ const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
196
199
  const stepSkillInput = normalizeSkillInput(s.skill);
197
200
  const stepOverrides: StepOverrides = { skills: stepSkillInput };
198
201
  const behavior = resolveStepBehavior(a, stepOverrides, chainSkills);
199
202
  const skillNames = behavior.skills === false ? [] : behavior.skills;
200
- const skillCwd = s.cwd ?? cwd ?? ctx.cwd;
201
- const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, skillCwd, ctx.cwd);
203
+ const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
202
204
 
203
- let systemPrompt = a.systemPrompt?.trim() || null;
205
+ let systemPrompt = a.systemPrompt?.trim() ?? "";
204
206
  if (resolvedSkills.length > 0) {
205
207
  const injection = buildSkillInjection(resolvedSkills);
206
208
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
207
209
  }
208
210
 
209
- const outputPath = resolveSingleOutputPath(s.output, ctx.cwd, s.cwd ?? cwd);
211
+ const outputPath = resolveSingleOutputPath(s.output, ctx.cwd, stepCwd);
210
212
  const task = injectSingleOutputInstruction(s.task ?? "{previous}", outputPath);
211
213
 
212
214
  const primaryModel = resolveModelCandidate(s.model ?? a.model, availableModels, ctx.currentModelProvider);
213
215
  return {
214
216
  agent: s.agent,
215
217
  task,
216
- cwd: s.cwd,
218
+ cwd: stepCwd,
217
219
  model: applyThinkingSuffix(primaryModel, a.thinking),
218
220
  modelCandidates: buildModelCandidates(s.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
219
221
  applyThinkingSuffix(candidate, a.thinking),
@@ -222,6 +224,9 @@ export function executeAsyncChain(
222
224
  extensions: a.extensions,
223
225
  mcpDirectTools: a.mcpDirectTools,
224
226
  systemPrompt,
227
+ systemPromptMode: a.systemPromptMode,
228
+ inheritProjectContext: a.inheritProjectContext,
229
+ inheritSkills: a.inheritSkills,
225
230
  skills: resolvedSkills.map((r) => r.name),
226
231
  outputPath,
227
232
  sessionFile,
@@ -255,7 +260,6 @@ export function executeAsyncChain(
255
260
  return buildSeqStep(s as SequentialStep, nextSessionFile());
256
261
  });
257
262
 
258
- const runnerCwd = cwd ?? ctx.cwd;
259
263
  let pid: number | undefined;
260
264
  try {
261
265
  pid = spawnRunner(
@@ -340,11 +344,11 @@ export function executeAsyncSingle(
340
344
  worktreeSetupHook,
341
345
  worktreeSetupHookTimeoutMs,
342
346
  } = params;
347
+ const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
343
348
  const skillNames = params.skills ?? agentConfig.skills ?? [];
344
349
  const availableModels = params.availableModels;
345
- const skillCwd = cwd ?? ctx.cwd;
346
- const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, skillCwd, ctx.cwd);
347
- let systemPrompt = agentConfig.systemPrompt?.trim() || null;
350
+ const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, runnerCwd, ctx.cwd);
351
+ let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
348
352
  if (resolvedSkills.length > 0) {
349
353
  const injection = buildSkillInjection(resolvedSkills);
350
354
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
@@ -362,8 +366,7 @@ export function executeAsyncSingle(
362
366
  };
363
367
  }
364
368
 
365
- const runnerCwd = cwd ?? ctx.cwd;
366
- const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, cwd);
369
+ const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, runnerCwd);
367
370
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
368
371
  let pid: number | undefined;
369
372
  try {
@@ -374,7 +377,7 @@ export function executeAsyncSingle(
374
377
  {
375
378
  agent,
376
379
  task: taskWithOutputInstruction,
377
- cwd,
380
+ cwd: runnerCwd,
378
381
  model: applyThinkingSuffix(resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider), agentConfig.thinking),
379
382
  modelCandidates: buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
380
383
  applyThinkingSuffix(candidate, agentConfig.thinking),
@@ -383,6 +386,9 @@ export function executeAsyncSingle(
383
386
  extensions: agentConfig.extensions,
384
387
  mcpDirectTools: agentConfig.mcpDirectTools,
385
388
  systemPrompt,
389
+ systemPromptMode: agentConfig.systemPromptMode,
390
+ inheritProjectContext: agentConfig.inheritProjectContext,
391
+ inheritSkills: agentConfig.inheritSkills,
386
392
  skills: resolvedSkills.map((r) => r.name),
387
393
  outputPath,
388
394
  sessionFile,
@@ -1,11 +1,11 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import * as path from "node:path";
3
- import { renderWidget } from "./render.js";
3
+ import { renderWidget } from "./render.ts";
4
4
  import {
5
5
  type SubagentState,
6
6
  POLL_INTERVAL_MS,
7
- } from "./types.js";
8
- import { readStatus } from "./utils.js";
7
+ } from "./types.ts";
8
+ import { readStatus } from "./utils.ts";
9
9
 
10
10
  export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string): {
11
11
  ensurePoller: () => void;
package/async-status.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { formatDuration, formatTokens, shortenPath } from "./formatters.js";
4
- import { type AsyncStatus, type TokenUsage } from "./types.js";
5
- import { readStatus } from "./utils.js";
3
+ import { formatDuration, formatTokens, shortenPath } from "./formatters.ts";
4
+ import { type AsyncStatus, type TokenUsage } from "./types.ts";
5
+ import { readStatus } from "./utils.ts";
6
6
 
7
7
  export interface AsyncRunStepSummary {
8
8
  index: number;
package/chain-clarify.ts CHANGED
@@ -987,7 +987,7 @@ export class ChainClarifyComponent implements Component {
987
987
  const prefix = isSelected ? th.fg("accent", "→ ") : " ";
988
988
  const modelText = isSelected ? th.fg("accent", model.id) : model.id;
989
989
  const providerBadge = th.fg("dim", ` [${model.provider}]`);
990
- const currentBadge = isCurrent ? th.fg("success", " ") : "";
990
+ const currentBadge = isCurrent ? th.fg("success", " current") : "";
991
991
 
992
992
  lines.push(this.row(` ${prefix}${modelText}${providerBadge}${currentBadge}`));
993
993
  }
@@ -1269,7 +1269,7 @@ export class ChainClarifyComponent implements Component {
1269
1269
  lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
1270
1270
 
1271
1271
  const progressEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
1272
- const progressValue = progressEnabled ? th.fg("success", "enabled") : th.fg("dim", "disabled");
1272
+ const progressValue = progressEnabled ? th.fg("success", "enabled") : th.fg("dim", "disabled");
1273
1273
  lines.push(this.row(` Progress: ${progressValue} ${th.fg("dim", "(press [p] to toggle)")}`));
1274
1274
  lines.push(this.row(""));
1275
1275
 
@@ -1331,8 +1331,8 @@ export class ChainClarifyComponent implements Component {
1331
1331
  if (progressEnabled) {
1332
1332
  const isFirstStep = i === 0;
1333
1333
  const progressAction = isFirstStep
1334
- ? th.fg("success", "●") + th.fg("dim", " creates & updates progress.md")
1335
- : th.fg("accent", "↔") + th.fg("dim", " reads & updates progress.md");
1334
+ ? th.fg("success", "writes progress.md")
1335
+ : th.fg("accent", "reads progress.md");
1336
1336
  const progressLabel = th.fg("dim", "progress: ");
1337
1337
  lines.push(this.row(` ${progressLabel}${progressAction}`));
1338
1338
  }
@@ -28,7 +28,7 @@ import {
28
28
  import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
29
29
  import { runSync } from "./execution.ts";
30
30
  import { buildChainSummary } from "./formatters.ts";
31
- import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
31
+ import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "./utils.ts";
32
32
  import { recordRun } from "./run-history.ts";
33
33
  import {
34
34
  cleanupWorktrees,
@@ -38,7 +38,7 @@ import {
38
38
  formatWorktreeDiffSummary,
39
39
  formatWorktreeTaskCwdConflict,
40
40
  type WorktreeSetup,
41
- } from "./worktree.js";
41
+ } from "./worktree.ts";
42
42
  import {
43
43
  type AgentProgress,
44
44
  type ArtifactConfig,
@@ -91,7 +91,7 @@ interface ParallelChainRunInput {
91
91
  }
92
92
 
93
93
  function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
94
- return {
94
+ return compactForegroundDetails({
95
95
  mode: "chain",
96
96
  results: input.results,
97
97
  progress: input.includeProgress ? input.allProgress : undefined,
@@ -99,7 +99,7 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
99
99
  chainAgents: input.chainAgents,
100
100
  totalSteps: input.totalSteps,
101
101
  currentStepIndex: input.currentStepIndex,
102
- };
102
+ });
103
103
  }
104
104
 
105
105
  function buildChainExecutionErrorResult(message: string, input: ChainExecutionDetailsInput): ChainExecutionResult {
@@ -181,7 +181,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
181
181
 
182
182
  const taskCwd = input.worktreeSetup
183
183
  ? input.worktreeSetup.worktrees[taskIndex]!.agentCwd
184
- : (task.cwd ?? input.cwd);
184
+ : resolveChildCwd(input.cwd ?? input.ctx.cwd, task.cwd);
185
185
 
186
186
  const outputPath = typeof behavior.output === "string"
187
187
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
@@ -412,7 +412,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
412
412
 
413
413
  if (isParallelStep(step)) {
414
414
  const parallelTemplates = stepTemplates as string[];
415
- const parallelCwd = step.cwd ?? cwd ?? ctx.cwd;
415
+ const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
416
416
  let worktreeSetup: WorktreeSetup | undefined;
417
417
  if (step.worktree) {
418
418
  const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(step.parallel, parallelCwd);
@@ -605,7 +605,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
605
605
  const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
606
606
 
607
607
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
608
- cwd: seqStep.cwd ?? cwd,
608
+ cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
609
609
  signal,
610
610
  runId,
611
611
  index: globalTaskIndex,
@@ -670,15 +670,16 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
670
670
  });
671
671
  return {
672
672
  content: [{ type: "text", text: summary }],
673
- details: {
674
- mode: "chain",
673
+ details: buildChainExecutionDetails({
675
674
  results,
676
- progress: includeProgress ? allProgress : undefined,
677
- artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
675
+ includeProgress,
676
+ allProgress,
677
+ allArtifactPaths,
678
+ artifactsDir,
678
679
  chainAgents,
679
680
  totalSteps,
680
681
  currentStepIndex: stepIndex,
681
- },
682
+ }),
682
683
  isError: true,
683
684
  };
684
685
  }
@@ -691,13 +692,14 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
691
692
 
692
693
  return {
693
694
  content: [{ type: "text", text: summary }],
694
- details: {
695
- mode: "chain",
695
+ details: buildChainExecutionDetails({
696
696
  results,
697
- progress: includeProgress ? allProgress : undefined,
698
- artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
697
+ includeProgress,
698
+ allProgress,
699
+ allArtifactPaths,
700
+ artifactsDir,
699
701
  chainAgents,
700
702
  totalSteps,
701
- },
703
+ }),
702
704
  };
703
705
  }
package/execution.ts CHANGED
@@ -72,7 +72,6 @@ async function runSingleAttempt(
72
72
  shared: {
73
73
  sessionEnabled: boolean;
74
74
  systemPrompt: string;
75
- skillNames: string[];
76
75
  resolvedSkillNames?: string[];
77
76
  skillsWarning?: string;
78
77
  jsonlPath?: string;
@@ -89,9 +88,11 @@ async function runSingleAttempt(
89
88
  sessionFile: options.sessionFile,
90
89
  model,
91
90
  thinking: agent.thinking,
91
+ systemPromptMode: agent.systemPromptMode,
92
+ inheritProjectContext: agent.inheritProjectContext,
93
+ inheritSkills: agent.inheritSkills,
92
94
  tools: agent.tools,
93
95
  extensions: agent.extensions,
94
- skills: shared.skillNames,
95
96
  systemPrompt: shared.systemPrompt,
96
97
  mcpDirectTools: agent.mcpDirectTools,
97
98
  promptFileStem: agent.name,
@@ -413,7 +414,6 @@ export async function runSync(
413
414
  const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
414
415
  sessionEnabled,
415
416
  systemPrompt,
416
- skillNames,
417
417
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
418
418
  skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
419
419
  jsonlPath,
package/formatters.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { Usage, SingleResult } from "./types.ts";
8
- import type { ChainStep, SequentialStep } from "./settings.ts";
8
+ import type { ChainStep } from "./settings.ts";
9
9
  import { isParallelStep } from "./settings.ts";
10
10
 
11
11
  /**
@@ -49,16 +49,13 @@ export function buildChainSummary(
49
49
  status: "completed" | "failed",
50
50
  failedStep?: { index: number; error: string },
51
51
  ): string {
52
- // Build step names for display
53
52
  const stepNames = steps
54
- .map((s) => (isParallelStep(s) ? `parallel[${s.parallel.length}]` : (s as SequentialStep).agent))
53
+ .map((step) => (isParallelStep(step) ? `parallel[${step.parallel.length}]` : step.agent))
55
54
  .join(" → ");
56
55
 
57
- // Calculate total duration from results
58
56
  const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0);
59
57
  const durationStr = formatDuration(totalDuration);
60
58
 
61
- // Check for progress.md
62
59
  const progressPath = path.join(chainDir, "progress.md");
63
60
  const hasProgress = fs.existsSync(progressPath);
64
61
  const allSkills = new Set<string>();
@@ -88,14 +85,20 @@ export function buildChainSummary(
88
85
  */
89
86
  export function formatToolCall(name: string, args: Record<string, unknown>): string {
90
87
  switch (name) {
91
- case "bash":
92
- return `$ ${((args.command as string) || "").slice(0, 60)}${(args.command as string)?.length > 60 ? "..." : ""}`;
88
+ case "bash": {
89
+ const command = typeof args.command === "string" ? args.command : "";
90
+ return `$ ${command.slice(0, 60)}${command.length > 60 ? "..." : ""}`;
91
+ }
93
92
  case "read":
94
- return `read ${shortenPath((args.path || args.file_path || "") as string)}`;
95
93
  case "write":
96
- return `write ${shortenPath((args.path || args.file_path || "") as string)}`;
97
- case "edit":
98
- return `edit ${shortenPath((args.path || args.file_path || "") as string)}`;
94
+ case "edit": {
95
+ const target = typeof args.path === "string"
96
+ ? args.path
97
+ : typeof args.file_path === "string"
98
+ ? args.file_path
99
+ : "";
100
+ return `${name} ${shortenPath(target)}`;
101
+ }
99
102
  default: {
100
103
  const s = JSON.stringify(args);
101
104
  return `${name} ${s.slice(0, 40)}${s.length > 40 ? "..." : ""}`;
@@ -108,7 +111,6 @@ export function formatToolCall(name: string, args: Record<string, unknown>): str
108
111
  */
109
112
  export function shortenPath(p: string): string {
110
113
  const home = process.env.HOME;
111
- // Only shorten if HOME is defined and non-empty, and path starts with it
112
114
  if (home && p.startsWith(home)) {
113
115
  return `~${p.slice(home.length)}`;
114
116
  }
package/index.ts CHANGED
@@ -266,7 +266,7 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
266
266
  MANAGEMENT (use action field, omit agent/task/chain/tasks):
267
267
  • { action: "list" } - discover agents/chains
268
268
  • { action: "get", agent: "name" } - full agent detail
269
- • { action: "create", config: { name, systemPrompt, ... } }
269
+ • { action: "create", config: { name, systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, ... } }
270
270
  • { action: "update", agent: "name", config: { ... } } - merge
271
271
  • { action: "delete", agent: "name" }
272
272
  • Use chainName for chain operations`,
package/install.mjs CHANGED
@@ -38,7 +38,7 @@ if (isRemove) {
38
38
  if (fs.existsSync(EXTENSION_DIR)) {
39
39
  console.log(`Removing ${EXTENSION_DIR}...`);
40
40
  fs.rmSync(EXTENSION_DIR, { recursive: true });
41
- console.log("pi-subagents removed");
41
+ console.log("pi-subagents removed");
42
42
  } else {
43
43
  console.log("pi-subagents is not installed");
44
44
  }
@@ -61,7 +61,7 @@ if (fs.existsSync(EXTENSION_DIR)) {
61
61
  console.log("Updating existing installation...");
62
62
  try {
63
63
  execSync("git pull", { cwd: EXTENSION_DIR, stdio: "inherit" });
64
- console.log("\n✓ pi-subagents updated");
64
+ console.log("\npi-subagents updated");
65
65
  } catch (err) {
66
66
  console.error("Failed to update. Try removing and reinstalling:");
67
67
  console.error(" npx pi-subagents --remove && npx pi-subagents");
@@ -77,7 +77,7 @@ if (fs.existsSync(EXTENSION_DIR)) {
77
77
  console.log(`Cloning to ${EXTENSION_DIR}...`);
78
78
  try {
79
79
  execSync(`git clone ${REPO_URL} "${EXTENSION_DIR}"`, { stdio: "inherit" });
80
- console.log("\n✓ pi-subagents installed");
80
+ console.log("\npi-subagents installed");
81
81
  } catch (err) {
82
82
  console.error("Failed to clone repository");
83
83
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
package/parallel-utils.ts CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * Parallel execution utilities for the async runner.
3
- * Kept minimal and self-contained so the standalone runner can use them
4
- * without pulling in the full extension dependency tree.
5
- */
6
-
7
- /** A single agent step in the runner config */
8
1
  export interface RunnerSubagentStep {
9
2
  agent: string;
10
3
  task: string;
@@ -15,13 +8,15 @@ export interface RunnerSubagentStep {
15
8
  extensions?: string[];
16
9
  mcpDirectTools?: string[];
17
10
  systemPrompt?: string | null;
11
+ systemPromptMode?: "append" | "replace";
12
+ inheritProjectContext: boolean;
13
+ inheritSkills: boolean;
18
14
  skills?: string[];
19
15
  outputPath?: string;
20
16
  sessionFile?: string;
21
17
  maxSubagentDepth?: number;
22
18
  }
23
19
 
24
- /** Parallel step group — multiple agents running concurrently */
25
20
  export interface ParallelStepGroup {
26
21
  parallel: RunnerSubagentStep[];
27
22
  concurrency?: number;
@@ -32,10 +27,9 @@ export interface ParallelStepGroup {
32
27
  export type RunnerStep = RunnerSubagentStep | ParallelStepGroup;
33
28
 
34
29
  export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
35
- return "parallel" in step && Array.isArray((step as ParallelStepGroup).parallel);
30
+ return "parallel" in step && Array.isArray(step.parallel);
36
31
  }
37
32
 
38
- /** Flatten runner steps into individual SubagentSteps for status tracking */
39
33
  export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
40
34
  const flat: RunnerSubagentStep[] = [];
41
35
  for (const step of steps) {
@@ -48,16 +42,12 @@ export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
48
42
  return flat;
49
43
  }
50
44
 
51
- /** Run async tasks with bounded concurrency, preserving result order.
52
- * `staggerMs` adds a small delay between each worker's start to avoid
53
- * file-lock contention when multiple subagents read shared config. */
54
45
  export async function mapConcurrent<T, R>(
55
46
  items: T[],
56
47
  limit: number,
57
48
  fn: (item: T, i: number) => Promise<R>,
58
49
  staggerMs = 150,
59
50
  ): Promise<R[]> {
60
- // Clamp to at least 1; NaN/undefined/0/negative all become 1
61
51
  const safeLimit = Math.max(1, Math.floor(limit) || 1);
62
52
  const results: R[] = new Array(items.length);
63
53
  let next = 0;
@@ -90,7 +80,6 @@ export interface ParallelTaskResult {
90
80
  outputTargetExists?: boolean;
91
81
  }
92
82
 
93
- /** Aggregate outputs from parallel tasks into a single string for {previous} */
94
83
  export function aggregateParallelOutputs(
95
84
  results: ParallelTaskResult[],
96
85
  headerFormat: (index: number, agent: string) => string = (i, agent) =>
@@ -102,15 +91,15 @@ export function aggregateParallelOutputs(
102
91
  const hasOutput = Boolean(r.output?.trim());
103
92
  const status =
104
93
  r.exitCode === -1
105
- ? "⏭️ SKIPPED"
94
+ ? "SKIPPED"
106
95
  : r.exitCode !== 0 && r.exitCode !== null
107
- ? `⚠️ FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
96
+ ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
108
97
  : r.error
109
- ? `⚠️ WARNING: ${r.error}`
98
+ ? `WARNING: ${r.error}`
110
99
  : !hasOutput && r.outputTargetPath && r.outputTargetExists === false
111
- ? `⚠️ EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
100
+ ? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
112
101
  : !hasOutput && !r.outputTargetPath
113
- ? "⚠️ EMPTY OUTPUT (no textual response returned)"
102
+ ? "EMPTY OUTPUT (no textual response returned)"
114
103
  : "";
115
104
  const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
116
105
  return `${header}\n${body}`;
package/pi-args.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
 
5
6
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
6
7
  const TASK_ARG_LIMIT = 8000;
8
+ const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
7
9
 
8
10
  export interface BuildPiArgsInput {
9
11
  baseArgs: string[];
@@ -13,9 +15,11 @@ export interface BuildPiArgsInput {
13
15
  sessionFile?: string;
14
16
  model?: string;
15
17
  thinking?: string;
18
+ systemPromptMode?: "append" | "replace";
19
+ inheritProjectContext: boolean;
20
+ inheritSkills: boolean;
16
21
  tools?: string[];
17
22
  extensions?: string[];
18
- skills?: string[];
19
23
  systemPrompt?: string | null;
20
24
  mcpDirectTools?: string[];
21
25
  promptFileStem?: string;
@@ -69,28 +73,29 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
69
73
  }
70
74
  }
71
75
 
76
+ const runtimeExtensions = [PROMPT_RUNTIME_EXTENSION_PATH];
72
77
  if (input.extensions !== undefined) {
73
78
  args.push("--no-extensions");
74
- for (const extPath of input.extensions) {
79
+ for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) {
75
80
  args.push("--extension", extPath);
76
81
  }
77
82
  } else {
78
- for (const extPath of toolExtensionPaths) {
83
+ for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths])]) {
79
84
  args.push("--extension", extPath);
80
85
  }
81
86
  }
82
87
 
83
- if ((input.skills?.length ?? 0) > 0) {
88
+ if (!input.inheritSkills) {
84
89
  args.push("--no-skills");
85
90
  }
86
91
 
87
92
  let tempDir: string | undefined;
88
- if (input.systemPrompt) {
93
+ if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
89
94
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
90
95
  const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
91
96
  const promptPath = path.join(tempDir, `${stem}.md`);
92
97
  fs.writeFileSync(promptPath, input.systemPrompt, { mode: 0o600 });
93
- args.push("--append-system-prompt", promptPath);
98
+ args.push(input.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt", promptPath);
94
99
  }
95
100
 
96
101
  if (input.task.length > TASK_ARG_LIMIT) {
@@ -105,6 +110,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
105
110
  }
106
111
 
107
112
  const env: Record<string, string | undefined> = {};
113
+ env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
114
+ env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
108
115
  if (input.mcpDirectTools?.length) {
109
116
  env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
110
117
  } else {
@@ -78,6 +78,7 @@ interface PromptTemplateBridgeResult {
78
78
  results?: Array<{
79
79
  agent?: string;
80
80
  messages?: unknown[];
81
+ finalOutput?: string;
81
82
  exitCode?: number;
82
83
  error?: string;
83
84
  model?: string;
@@ -180,12 +181,13 @@ function sanitizeRecentTools(
180
181
  tools: Array<{ tool?: string; args?: string }> | undefined,
181
182
  ): Array<{ tool: string; args: string }> | undefined {
182
183
  if (!tools || tools.length === 0) return undefined;
183
- const sanitized = tools
184
- .filter((entry) => typeof entry.tool === "string" && entry.tool.trim().length > 0)
185
- .map((entry) => ({
186
- tool: entry.tool as string,
184
+ const sanitized = tools.flatMap((entry) => {
185
+ if (typeof entry.tool !== "string" || entry.tool.trim().length === 0) return [];
186
+ return [{
187
+ tool: entry.tool,
187
188
  args: typeof entry.args === "string" ? entry.args : String(entry.args ?? ""),
188
- }));
189
+ }];
190
+ });
189
191
  return sanitized.length > 0 ? sanitized : undefined;
190
192
  }
191
193
 
@@ -207,6 +209,15 @@ function resolveProgressModel(
207
209
  return firstWithModel?.model;
208
210
  }
209
211
 
212
+ function buildDelegationMessages(result: { messages?: unknown[]; finalOutput?: string }, fallbackText?: string): unknown[] {
213
+ if (Array.isArray(result.messages) && result.messages.length > 0) return result.messages;
214
+ const text = typeof result.finalOutput === "string" && result.finalOutput.trim().length > 0
215
+ ? result.finalOutput.trim()
216
+ : fallbackText;
217
+ if (!text) return [];
218
+ return [{ role: "assistant", content: [{ type: "text", text }] }];
219
+ }
220
+
210
221
  function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
211
222
  const progress = update.details?.progress?.[0];
212
223
  const taskProgress = update.details?.progress?.map((entry) => {
@@ -324,7 +335,8 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
324
335
  options.events.emit(PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT, payload);
325
336
  },
326
337
  );
327
- const messages = result.details?.results?.[0]?.messages ?? [];
338
+ const contentText = firstTextContent(result.content);
339
+ const messages = buildDelegationMessages(result.details?.results?.[0] ?? {}, contentText);
328
340
  const parallelResults = request.tasks
329
341
  ? request.tasks.map<PromptTemplateDelegationParallelResult>((task, index) => {
330
342
  const step = result.details?.results?.[index];
@@ -340,13 +352,12 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
340
352
  const errorText = step.error;
341
353
  return {
342
354
  agent: step.agent ?? task.agent,
343
- messages: step.messages ?? [],
355
+ messages: buildDelegationMessages(step),
344
356
  isError: (exitCode !== undefined && exitCode !== 0) || !!errorText,
345
357
  errorText: errorText || undefined,
346
358
  };
347
359
  })
348
360
  : undefined;
349
- const contentText = firstTextContent(result.content);
350
361
  const response: PromptTemplateDelegationResponse = {
351
362
  ...request,
352
363
  messages,