pi-subagents 0.14.1 → 0.16.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.
package/agents.ts CHANGED
@@ -14,11 +14,27 @@ import { parseFrontmatter } from "./frontmatter.ts";
14
14
  export type AgentScope = "user" | "project" | "both";
15
15
 
16
16
  export type AgentSource = "builtin" | "user" | "project";
17
+ export type SystemPromptMode = "append" | "replace";
18
+
19
+ export function defaultSystemPromptMode(name: string): SystemPromptMode {
20
+ return name === "delegate" ? "append" : "replace";
21
+ }
22
+
23
+ export function defaultInheritProjectContext(name: string): boolean {
24
+ return name === "delegate";
25
+ }
26
+
27
+ export function defaultInheritSkills(): boolean {
28
+ return false;
29
+ }
17
30
 
18
31
  export interface BuiltinAgentOverrideBase {
19
32
  model?: string;
20
33
  fallbackModels?: string[];
21
34
  thinking?: string;
35
+ systemPromptMode: SystemPromptMode;
36
+ inheritProjectContext: boolean;
37
+ inheritSkills: boolean;
22
38
  systemPrompt: string;
23
39
  skills?: string[];
24
40
  tools?: string[];
@@ -29,6 +45,9 @@ export interface BuiltinAgentOverrideConfig {
29
45
  model?: string | false;
30
46
  fallbackModels?: string[] | false;
31
47
  thinking?: string | false;
48
+ systemPromptMode?: SystemPromptMode;
49
+ inheritProjectContext?: boolean;
50
+ inheritSkills?: boolean;
32
51
  systemPrompt?: string;
33
52
  skills?: string[] | false;
34
53
  tools?: string[] | false;
@@ -48,6 +67,9 @@ export interface AgentConfig {
48
67
  model?: string;
49
68
  fallbackModels?: string[];
50
69
  thinking?: string;
70
+ systemPromptMode: SystemPromptMode;
71
+ inheritProjectContext: boolean;
72
+ inheritSkills: boolean;
51
73
  systemPrompt: string;
52
74
  source: AgentSource;
53
75
  filePath: string;
@@ -125,6 +147,9 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
125
147
  model: agent.model,
126
148
  fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : undefined,
127
149
  thinking: agent.thinking,
150
+ systemPromptMode: agent.systemPromptMode,
151
+ inheritProjectContext: agent.inheritProjectContext,
152
+ inheritSkills: agent.inheritSkills,
128
153
  systemPrompt: agent.systemPrompt,
129
154
  skills: agent.skills ? [...agent.skills] : undefined,
130
155
  tools: agent.tools ? [...agent.tools] : undefined,
@@ -139,6 +164,9 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
139
164
  ? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
140
165
  : {}),
141
166
  ...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
167
+ ...(override.systemPromptMode !== undefined ? { systemPromptMode: override.systemPromptMode } : {}),
168
+ ...(override.inheritProjectContext !== undefined ? { inheritProjectContext: override.inheritProjectContext } : {}),
169
+ ...(override.inheritSkills !== undefined ? { inheritSkills: override.inheritSkills } : {}),
142
170
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
143
171
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
144
172
  ...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
@@ -187,29 +215,85 @@ function writeSettingsFile(filePath: string, settings: Record<string, unknown>):
187
215
  fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
188
216
  }
189
217
 
190
- function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
218
+ function parseOverrideStringArrayOrFalse(
219
+ value: unknown,
220
+ meta: { filePath: string; name: string; field: string },
221
+ ): string[] | false | undefined {
222
+ if (value === undefined) return undefined;
191
223
  if (value === false) return false;
192
- if (!Array.isArray(value)) return undefined;
193
- const items = value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean);
224
+ if (!Array.isArray(value)) {
225
+ throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`);
226
+ }
227
+
228
+ const items: string[] = [];
229
+ for (const item of value) {
230
+ if (typeof item !== "string") {
231
+ throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`);
232
+ }
233
+ const trimmed = item.trim();
234
+ if (trimmed) items.push(trimmed);
235
+ }
194
236
  return items;
195
237
  }
196
238
 
197
- function parseBuiltinOverrideEntry(value: unknown): BuiltinAgentOverrideConfig | undefined {
198
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
239
+ function parseBuiltinOverrideEntry(
240
+ name: string,
241
+ value: unknown,
242
+ filePath: string,
243
+ ): BuiltinAgentOverrideConfig | undefined {
244
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
245
+ throw new Error(`Builtin override '${name}' in '${filePath}' must be an object.`);
246
+ }
247
+
199
248
  const input = value as Record<string, unknown>;
200
249
  const override: BuiltinAgentOverrideConfig = {};
201
250
 
202
- if (typeof input.model === "string" || input.model === false) override.model = input.model;
203
- if (typeof input.thinking === "string" || input.thinking === false) override.thinking = input.thinking;
204
- if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
251
+ if ("model" in input) {
252
+ if (typeof input.model === "string" || input.model === false) override.model = input.model;
253
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
254
+ }
255
+
256
+ if ("thinking" in input) {
257
+ if (typeof input.thinking === "string" || input.thinking === false) override.thinking = input.thinking;
258
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected a string or false.`);
259
+ }
260
+
261
+ if ("systemPromptMode" in input) {
262
+ if (input.systemPromptMode === "append" || input.systemPromptMode === "replace") {
263
+ override.systemPromptMode = input.systemPromptMode;
264
+ } else {
265
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPromptMode'; expected 'append' or 'replace'.`);
266
+ }
267
+ }
205
268
 
206
- const fallbackModels = parseStringArrayOrFalse(input.fallbackModels);
269
+ if ("inheritProjectContext" in input) {
270
+ if (typeof input.inheritProjectContext === "boolean") {
271
+ override.inheritProjectContext = input.inheritProjectContext;
272
+ } else {
273
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritProjectContext'; expected a boolean.`);
274
+ }
275
+ }
276
+
277
+ if ("inheritSkills" in input) {
278
+ if (typeof input.inheritSkills === "boolean") {
279
+ override.inheritSkills = input.inheritSkills;
280
+ } else {
281
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritSkills'; expected a boolean.`);
282
+ }
283
+ }
284
+
285
+ if ("systemPrompt" in input) {
286
+ if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
287
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`);
288
+ }
289
+
290
+ const fallbackModels = parseOverrideStringArrayOrFalse(input.fallbackModels, { filePath, name, field: "fallbackModels" });
207
291
  if (fallbackModels !== undefined) override.fallbackModels = fallbackModels;
208
292
 
209
- const skills = parseStringArrayOrFalse(input.skills);
293
+ const skills = parseOverrideStringArrayOrFalse(input.skills, { filePath, name, field: "skills" });
210
294
  if (skills !== undefined) override.skills = skills;
211
295
 
212
- const tools = parseStringArrayOrFalse(input.tools);
296
+ const tools = parseOverrideStringArrayOrFalse(input.tools, { filePath, name, field: "tools" });
213
297
  if (tools !== undefined) override.tools = tools;
214
298
 
215
299
  return Object.keys(override).length > 0 ? override : undefined;
@@ -225,7 +309,7 @@ function readBuiltinOverrides(filePath: string | null): Record<string, BuiltinAg
225
309
 
226
310
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
227
311
  for (const [name, value] of Object.entries(agentOverrides)) {
228
- const override = parseBuiltinOverrideEntry(value);
312
+ const override = parseBuiltinOverrideEntry(name, value, filePath);
229
313
  if (override) parsed[name] = override;
230
314
  }
231
315
  return parsed;
@@ -246,6 +330,9 @@ function applyBuiltinOverride(
246
330
  next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels];
247
331
  }
248
332
  if (override.thinking !== undefined) next.thinking = override.thinking === false ? undefined : override.thinking;
333
+ if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
334
+ if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
335
+ if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills;
249
336
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
250
337
  if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
251
338
  if (override.tools !== undefined) {
@@ -281,13 +368,16 @@ function applyBuiltinOverrides(
281
368
 
282
369
  export function buildBuiltinOverrideConfig(
283
370
  base: BuiltinAgentOverrideBase,
284
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
371
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
285
372
  ): BuiltinAgentOverrideConfig | undefined {
286
373
  const override: BuiltinAgentOverrideConfig = {};
287
374
 
288
375
  if (draft.model !== base.model) override.model = draft.model ?? false;
289
376
  if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
290
377
  if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
378
+ if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode;
379
+ if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext;
380
+ if (draft.inheritSkills !== base.inheritSkills) override.inheritSkills = draft.inheritSkills;
291
381
  if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt;
292
382
  if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false;
293
383
 
@@ -410,6 +500,21 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
410
500
  ?.split(",")
411
501
  .map((model) => model.trim())
412
502
  .filter(Boolean);
503
+ const systemPromptMode = frontmatter.systemPromptMode === "replace"
504
+ ? "replace"
505
+ : frontmatter.systemPromptMode === "append"
506
+ ? "append"
507
+ : defaultSystemPromptMode(frontmatter.name);
508
+ const inheritProjectContext = frontmatter.inheritProjectContext === "true"
509
+ ? true
510
+ : frontmatter.inheritProjectContext === "false"
511
+ ? false
512
+ : defaultInheritProjectContext(frontmatter.name);
513
+ const inheritSkills = frontmatter.inheritSkills === "true"
514
+ ? true
515
+ : frontmatter.inheritSkills === "false"
516
+ ? false
517
+ : defaultInheritSkills();
413
518
 
414
519
  let extensions: string[] | undefined;
415
520
  if (frontmatter.extensions !== undefined) {
@@ -434,6 +539,9 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
434
539
  model: frontmatter.model,
435
540
  fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined,
436
541
  thinking: frontmatter.thinking,
542
+ systemPromptMode,
543
+ inheritProjectContext,
544
+ inheritSkills,
437
545
  systemPrompt: body,
438
546
  source,
439
547
  filePath,
@@ -498,13 +606,20 @@ function isDirectory(p: string): boolean {
498
606
  }
499
607
  }
500
608
 
501
- function findNearestProjectAgentsDir(cwd: string): string | null {
609
+ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
502
610
  const projectRoot = findNearestProjectRoot(cwd);
503
- if (!projectRoot) return null;
504
- const candidateAlt = path.join(projectRoot, ".agents");
505
- if (isDirectory(candidateAlt)) return candidateAlt;
506
- const candidate = path.join(projectRoot, ".pi", "agents");
507
- return isDirectory(candidate) ? candidate : null;
611
+ if (!projectRoot) return { readDirs: [], preferredDir: null };
612
+
613
+ const legacyDir = path.join(projectRoot, ".agents");
614
+ const preferredDir = path.join(projectRoot, ".pi", "agents");
615
+ const readDirs: string[] = [];
616
+ if (isDirectory(legacyDir)) readDirs.push(legacyDir);
617
+ if (isDirectory(preferredDir)) readDirs.push(preferredDir);
618
+
619
+ return {
620
+ readDirs,
621
+ preferredDir,
622
+ };
508
623
  }
509
624
 
510
625
  const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "agents");
@@ -512,7 +627,7 @@ const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)
512
627
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
513
628
  const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
514
629
  const userDirNew = path.join(os.homedir(), ".agents");
515
- const projectAgentsDir = findNearestProjectAgentsDir(cwd);
630
+ const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
516
631
  const userSettingsPath = getUserAgentSettingsPath();
517
632
  const projectSettingsPath = getProjectAgentSettingsPath(cwd);
518
633
 
@@ -523,12 +638,12 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
523
638
  userSettingsPath,
524
639
  projectSettingsPath,
525
640
  );
526
-
641
+
527
642
  const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
528
643
  const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
529
644
  const userAgents = [...userAgentsOld, ...userAgentsNew];
530
645
 
531
- const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
646
+ const projectAgents = scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project"));
532
647
  const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents);
533
648
 
534
649
  return { agents, projectAgentsDir };
@@ -546,7 +661,7 @@ export function discoverAgentsAll(cwd: string): {
546
661
  } {
547
662
  const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
548
663
  const userDirNew = path.join(os.homedir(), ".agents");
549
- const projectDir = findNearestProjectAgentsDir(cwd);
664
+ const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
550
665
  const userSettingsPath = getUserAgentSettingsPath();
551
666
  const projectSettingsPath = getProjectAgentSettingsPath(cwd);
552
667
 
@@ -561,11 +676,24 @@ export function discoverAgentsAll(cwd: string): {
561
676
  ...loadAgentsFromDir(userDirOld, "user"),
562
677
  ...loadAgentsFromDir(userDirNew, "user"),
563
678
  ];
564
- const project = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
679
+ const projectMap = new Map<string, AgentConfig>();
680
+ for (const dir of projectDirs) {
681
+ for (const agent of loadAgentsFromDir(dir, "project")) {
682
+ projectMap.set(agent.name, agent);
683
+ }
684
+ }
685
+ const project = Array.from(projectMap.values());
686
+
687
+ const chainMap = new Map<string, ChainConfig>();
688
+ for (const dir of projectDirs) {
689
+ for (const chain of loadChainsFromDir(dir, "project")) {
690
+ chainMap.set(chain.name, chain);
691
+ }
692
+ }
565
693
  const chains = [
566
694
  ...loadChainsFromDir(userDirOld, "user"),
567
695
  ...loadChainsFromDir(userDirNew, "user"),
568
- ...(projectDir ? loadChainsFromDir(projectDir, "project") : []),
696
+ ...Array.from(chainMap.values()),
569
697
  ];
570
698
 
571
699
  const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
@@ -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;
@@ -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 { compactForegroundDetails, 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,
@@ -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,
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/fork-context.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  export type SubagentExecutionContext = "fresh" | "fork";
2
2
 
3
+ interface ForkableSessionManagerStatic {
4
+ open(path: string): { createBranchedSession(leafId: string): string | undefined };
5
+ }
6
+
3
7
  export interface ForkableSessionManager {
4
8
  getSessionFile(): string | undefined;
5
9
  getLeafId(): string | null;
6
- createBranchedSession(leafId: string): string | undefined;
10
+ constructor: ForkableSessionManagerStatic;
7
11
  }
8
12
 
9
13
  export interface ForkContextResolver {
@@ -41,7 +45,8 @@ export function createForkContextResolver(
41
45
  const cached = cachedSessionFiles.get(index);
42
46
  if (cached) return cached;
43
47
  try {
44
- const sessionFile = sessionManager.createBranchedSession(leafId);
48
+ const sourceManager = sessionManager.constructor.open(parentSessionFile);
49
+ const sessionFile = sourceManager.createBranchedSession(leafId);
45
50
  if (!sessionFile) {
46
51
  throw new Error("Session manager did not return a session file.");
47
52
  }
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>();
@@ -86,19 +83,27 @@ export function buildChainSummary(
86
83
  /**
87
84
  * Format a tool call for display
88
85
  */
89
- export function formatToolCall(name: string, args: Record<string, unknown>): string {
86
+ export function formatToolCall(name: string, args: Record<string, unknown>, expanded = false): 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
+ const maxLength = expanded ? 240 : 60;
91
+ return `$ ${command.slice(0, maxLength)}${command.length > maxLength ? "..." : ""}`;
92
+ }
93
93
  case "read":
94
- return `read ${shortenPath((args.path || args.file_path || "") as string)}`;
95
94
  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)}`;
95
+ case "edit": {
96
+ const target = typeof args.path === "string"
97
+ ? args.path
98
+ : typeof args.file_path === "string"
99
+ ? args.file_path
100
+ : "";
101
+ return `${name} ${shortenPath(target)}`;
102
+ }
99
103
  default: {
100
104
  const s = JSON.stringify(args);
101
- return `${name} ${s.slice(0, 40)}${s.length > 40 ? "..." : ""}`;
105
+ const maxLength = expanded ? 160 : 40;
106
+ return `${name} ${s.slice(0, maxLength)}${s.length > maxLength ? "..." : ""}`;
102
107
  }
103
108
  }
104
109
  }
@@ -108,7 +113,6 @@ export function formatToolCall(name: string, args: Record<string, unknown>): str
108
113
  */
109
114
  export function shortenPath(p: string): string {
110
115
  const home = process.env.HOME;
111
- // Only shorten if HOME is defined and non-empty, and path starts with it
112
116
  if (home && p.startsWith(home)) {
113
117
  return `~${p.slice(home.length)}`;
114
118
  }
package/index.ts CHANGED
@@ -253,7 +253,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
253
253
  EXECUTION (use exactly ONE mode):
254
254
  • SINGLE: { agent, task } - one task
255
255
  • CHAIN: { chain: [{agent:"scout"}, {parallel:[{agent:"worker",count:3}]}] } - sequential pipeline with optional parallel fan-out
256
- • PARALLEL: { tasks: [{agent,task,count?}, ...], worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
256
+ • PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
257
257
  • Optional context: { context: "fresh" | "fork" } (default: "fresh")
258
258
 
259
259
  CHAIN TEMPLATE VARIABLES (use in task strings):
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.14.1",
3
+ "version": "0.16.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",