pi-subagents 0.24.3 → 0.24.4

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/CHANGELOG.md CHANGED
@@ -1,19 +1,32 @@
1
1
  # Changelog
2
2
 
3
- ## [0.24.3] - 2026-05-14
3
+ ## [Unreleased]
4
4
 
5
5
  ### Added
6
- - Show provider-free model and thinking labels in async subagent widgets and status views.
7
- - Added a packaged `/review-loop` prompt for parent-controlled worker, fresh-reviewer, and fix-worker cycles that can run as an initial async chain or as follow-up subagent runs after async worker completions, stopping when reviewers find no fixes worth doing now or the review-round cap is reached.
8
6
 
9
7
  ### Fixed
10
- - Let `async: true` chain tool calls run in the background when `clarify` is omitted, and avoid showing the async badge for explicit foreground clarify runs.
11
8
 
12
- ## [Unreleased]
9
+ ## [0.24.4] - 2026-05-20
10
+
11
+ ### Fixed
12
+ - Treat provider-coerced single-run `output: "false"` the same as boolean `false`, preventing literal `false` output files in foreground and async runs.
13
+ - Include selected direct MCP tool names in explicit child `--tools` allowlists when metadata cache/config resolution is available.
14
+ - Honor `PI_CODING_AGENT_DIR` for runtime config, agent/chain/settings discovery, skills, run history, artifact cleanup, and intercom defaults.
15
+ - Hide nested child Pi process windows on Windows for both foreground and background subagent runs.
16
+ - Avoid completion-guard false positives for declared read-only agents, and add `completionGuard: false` for bash-enabled non-implementation agents that should not be required to edit files.
17
+ - Skip empty or whitespace-only assistant text parts when selecting subagent final output, so later meaningful text in the same or earlier assistant message is not masked.
18
+ - Declare `@earendil-works/pi-tui` as a runtime dependency so packaged installs can load the extension without relying on dev dependencies or optional peers.
19
+ - Treat recovered intermediate child tool/provider errors as successful when a later clean final assistant response is emitted, preventing false failed subagent results.
20
+ - Use progress-driven spinner frames in subagent result rows and async widgets, avoiding timer-driven off-screen redraw flicker in small terminals.
21
+
22
+ ## [0.24.3] - 2026-05-14
13
23
 
14
24
  ### Added
25
+ - Show provider-free model and thinking labels in async subagent widgets and status views.
26
+ - Added a packaged `/review-loop` prompt for parent-controlled worker, fresh-reviewer, and fix-worker cycles that can run as an initial async chain or as follow-up subagent runs after async worker completions, stopping when reviewers find no fixes worth doing now or the review-round cap is reached.
15
27
 
16
28
  ### Fixed
29
+ - Let `async: true` chain tool calls run in the background when `clarify` is omitted, and avoid showing the async badge for explicit foreground clarify runs.
17
30
 
18
31
  ## [0.24.2] - 2026-05-10
19
32
 
package/README.md CHANGED
@@ -433,6 +433,7 @@ skills: safe-bash, chrome-devtools
433
433
  output: context.md
434
434
  defaultReads: context.md
435
435
  defaultProgress: true
436
+ completionGuard: false
436
437
  interactive: true
437
438
  maxSubagentDepth: 1
438
439
  ---
@@ -458,12 +459,13 @@ Important fields:
458
459
  | `output` | Default single-agent output file. |
459
460
  | `defaultReads` | Files to read before running in chain/parallel behavior. |
460
461
  | `defaultProgress` | Maintain `progress.md`. |
462
+ | `completionGuard` | Set `false` only for non-implementation agents that may mention implementation words while using mutation-capable tools such as `bash`. |
461
463
  | `interactive` | Parsed for compatibility but not enforced in v1. |
462
464
  | `maxSubagentDepth` | Tightens nested delegation for this agent’s children. |
463
465
 
464
466
  ### Tool and extension selection
465
467
 
466
- If `tools` is omitted, `pi-subagents` does not pass `--tools`, so the child gets Pi’s normal builtin tools. If `tools` is present, regular tool names become an explicit allowlist. `mcp:` entries are split out and forwarded as direct MCP selections. Path-like `tools` entries, such as extension paths or `.ts`/`.js` files, are treated as tool-extension paths rather than builtin tool names.
468
+ If `tools` is omitted, `pi-subagents` does not pass `--tools`, so the child gets Pi’s normal builtin tools. If `tools` is present, regular tool names become an explicit allowlist. `mcp:` entries are split out and forwarded as direct MCP selections. Path-like `tools` entries, such as extension paths or `.ts`/`.js` files, are treated as tool-extension paths rather than builtin tool names. Agents that declare only known read-only builtin tools skip the implementation completion guard, but `bash`, unknown tools, and MCP tools stay mutation-capable. Use `completionGuard: false` for bash-enabled validators or advisors that should never be judged as implementation agents.
467
469
 
468
470
  Examples:
469
471
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.24.3",
3
+ "version": "0.24.4",
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",
@@ -54,8 +54,7 @@
54
54
  "peerDependencies": {
55
55
  "@earendil-works/pi-agent-core": "*",
56
56
  "@earendil-works/pi-ai": "*",
57
- "@earendil-works/pi-coding-agent": "*",
58
- "@earendil-works/pi-tui": "*"
57
+ "@earendil-works/pi-coding-agent": "*"
59
58
  },
60
59
  "peerDependenciesMeta": {
61
60
  "@earendil-works/pi-agent-core": {
@@ -66,19 +65,16 @@
66
65
  },
67
66
  "@earendil-works/pi-coding-agent": {
68
67
  "optional": true
69
- },
70
- "@earendil-works/pi-tui": {
71
- "optional": true
72
68
  }
73
69
  },
74
70
  "dependencies": {
71
+ "@earendil-works/pi-tui": "^0.74.0",
75
72
  "jiti": "^2.7.0",
76
73
  "typebox": "^1.1.24"
77
74
  },
78
75
  "devDependencies": {
79
76
  "@earendil-works/pi-agent-core": "^0.74.0",
80
77
  "@earendil-works/pi-ai": "^0.74.0",
81
- "@earendil-works/pi-coding-agent": "^0.74.0",
82
- "@earendil-works/pi-tui": "^0.74.0"
78
+ "@earendil-works/pi-coding-agent": "^0.74.0"
83
79
  }
84
80
  }
@@ -297,6 +297,10 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
297
297
  target.maxSubagentDepth = cfg.maxSubagentDepth;
298
298
  } else return "config.maxSubagentDepth must be an integer >= 0 or false when provided.";
299
299
  }
300
+ if (hasKey(cfg, "completionGuard")) {
301
+ if (typeof cfg.completionGuard !== "boolean") return "config.completionGuard must be a boolean when provided.";
302
+ target.completionGuard = cfg.completionGuard;
303
+ }
300
304
  return undefined;
301
305
  }
302
306
 
@@ -366,6 +370,7 @@ function formatAgentDetail(agent: AgentConfig): string {
366
370
  if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
367
371
  if (agent.defaultProgress) lines.push("Progress: true");
368
372
  if (agent.maxSubagentDepth !== undefined) lines.push(`Max subagent depth: ${agent.maxSubagentDepth}`);
373
+ if (agent.completionGuard === false) lines.push("Completion guard: false");
369
374
  if (agent.systemPrompt.trim()) lines.push("", "System Prompt:", agent.systemPrompt);
370
375
  return lines.join("\n");
371
376
  }
@@ -21,6 +21,7 @@ export const KNOWN_FIELDS = new Set([
21
21
  "defaultProgress",
22
22
  "interactive",
23
23
  "maxSubagentDepth",
24
+ "completionGuard",
24
25
  ]);
25
26
 
26
27
  function joinComma(values: string[] | undefined): string | undefined {
@@ -69,6 +70,7 @@ export function serializeAgent(config: AgentConfig): string {
69
70
  if (Number.isInteger(config.maxSubagentDepth) && config.maxSubagentDepth >= 0) {
70
71
  lines.push(`maxSubagentDepth: ${config.maxSubagentDepth}`);
71
72
  }
73
+ if (config.completionGuard === false) lines.push("completionGuard: false");
72
74
 
73
75
  if (config.extraFields) {
74
76
  for (const [key, value] of Object.entries(config.extraFields)) {
@@ -7,6 +7,7 @@ import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import type { OutputMode } from "../shared/types.ts";
10
+ import { getAgentDir } from "../shared/utils.ts";
10
11
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
11
12
  import { parseChain } from "./chain-serializer.ts";
12
13
  import { mergeAgentsForScope } from "./agent-selection.ts";
@@ -45,6 +46,7 @@ export interface BuiltinAgentOverrideBase {
45
46
  skills?: string[];
46
47
  tools?: string[];
47
48
  mcpDirectTools?: string[];
49
+ completionGuard?: boolean;
48
50
  }
49
51
 
50
52
  interface BuiltinAgentOverrideConfig {
@@ -59,6 +61,7 @@ interface BuiltinAgentOverrideConfig {
59
61
  systemPrompt?: string;
60
62
  skills?: string[] | false;
61
63
  tools?: string[] | false;
64
+ completionGuard?: boolean;
62
65
  }
63
66
 
64
67
  interface BuiltinAgentOverrideInfo {
@@ -91,6 +94,7 @@ export interface AgentConfig {
91
94
  defaultProgress?: boolean;
92
95
  interactive?: boolean;
93
96
  maxSubagentDepth?: number;
97
+ completionGuard?: boolean;
94
98
  disabled?: boolean;
95
99
  extraFields?: Record<string, string>;
96
100
  override?: BuiltinAgentOverrideInfo;
@@ -131,7 +135,7 @@ interface AgentDiscoveryResult {
131
135
  }
132
136
 
133
137
  function getUserChainDir(): string {
134
- return path.join(os.homedir(), ".pi", "agent", "chains");
138
+ return path.join(getAgentDir(), "chains");
135
139
  }
136
140
 
137
141
  function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
@@ -182,6 +186,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
182
186
  skills: agent.skills ? [...agent.skills] : undefined,
183
187
  tools: agent.tools ? [...agent.tools] : undefined,
184
188
  mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
189
+ completionGuard: agent.completionGuard,
185
190
  };
186
191
  }
187
192
 
@@ -200,6 +205,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
200
205
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
201
206
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
202
207
  ...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
208
+ ...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
203
209
  };
204
210
  }
205
211
 
@@ -217,7 +223,7 @@ function findNearestProjectRoot(cwd: string): string | null {
217
223
  }
218
224
 
219
225
  function getUserAgentSettingsPath(): string {
220
- return path.join(os.homedir(), ".pi", "agent", "settings.json");
226
+ return path.join(getAgentDir(), "settings.json");
221
227
  }
222
228
 
223
229
  function getProjectAgentSettingsPath(cwd: string): string | null {
@@ -336,6 +342,14 @@ function parseBuiltinOverrideEntry(
336
342
  }
337
343
  }
338
344
 
345
+ if ("completionGuard" in input) {
346
+ if (typeof input.completionGuard === "boolean") {
347
+ override.completionGuard = input.completionGuard;
348
+ } else {
349
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'completionGuard'; expected a boolean.`);
350
+ }
351
+ }
352
+
339
353
  if ("systemPrompt" in input) {
340
354
  if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
341
355
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`);
@@ -408,6 +422,7 @@ function applyBuiltinOverride(
408
422
  next.tools = tools;
409
423
  next.mcpDirectTools = mcpDirectTools;
410
424
  }
425
+ if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
411
426
 
412
427
  return next;
413
428
  }
@@ -447,7 +462,7 @@ function applyBuiltinOverrides(
447
462
 
448
463
  export function buildBuiltinOverrideConfig(
449
464
  base: BuiltinAgentOverrideBase,
450
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
465
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "completionGuard">,
451
466
  ): BuiltinAgentOverrideConfig | undefined {
452
467
  const override: BuiltinAgentOverrideConfig = {};
453
468
 
@@ -465,6 +480,9 @@ export function buildBuiltinOverrideConfig(
465
480
  const baseTools = joinToolList(base);
466
481
  const draftTools = joinToolList(draft);
467
482
  if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
483
+ if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
484
+ override.completionGuard = draft.completionGuard !== false;
485
+ }
468
486
 
469
487
  return Object.keys(override).length > 0 ? override : undefined;
470
488
  }
@@ -630,6 +648,11 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
630
648
  }
631
649
 
632
650
  const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
651
+ const completionGuard = frontmatter.completionGuard === "false"
652
+ ? false
653
+ : frontmatter.completionGuard === "true"
654
+ ? true
655
+ : undefined;
633
656
 
634
657
  agents.push({
635
658
  name: runtimeName,
@@ -658,6 +681,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
658
681
  Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
659
682
  ? parsedMaxSubagentDepth
660
683
  : undefined,
684
+ completionGuard,
661
685
  extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
662
686
  });
663
687
  }
@@ -723,7 +747,7 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
723
747
  const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
724
748
 
725
749
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
726
- const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
750
+ const userDirOld = path.join(getAgentDir(), "agents");
727
751
  const userDirNew = path.join(os.homedir(), ".agents");
728
752
  const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
729
753
  const userSettingsPath = getUserAgentSettingsPath();
@@ -762,7 +786,7 @@ export function discoverAgentsAll(cwd: string): {
762
786
  userSettingsPath: string;
763
787
  projectSettingsPath: string | null;
764
788
  } {
765
- const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
789
+ const userDirOld = path.join(getAgentDir(), "agents");
766
790
  const userDirNew = path.join(os.homedir(), ".agents");
767
791
  const userChainDir = getUserChainDir();
768
792
  const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
@@ -802,7 +826,7 @@ export function discoverAgentsAll(cwd: string): {
802
826
  ...Array.from(chainMap.values()),
803
827
  ];
804
828
 
805
- const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
829
+ const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
806
830
 
807
831
  return { builtin, user, project, chains, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
808
832
  }
@@ -6,6 +6,7 @@ import { execSync } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
+ import { getAgentDir } from "../shared/utils.ts";
9
10
 
10
11
  export type SkillSource =
11
12
  | "project"
@@ -46,11 +47,10 @@ interface SkillSearchPath {
46
47
  const skillCache = new Map<string, SkillCacheEntry>();
47
48
  const MAX_CACHE_SIZE = 50;
48
49
 
49
- let loadSkillsCache: { cwd: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
50
+ let loadSkillsCache: { cwd: string; agentDir: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
50
51
  const LOAD_SKILLS_CACHE_TTL_MS = 5000;
51
52
 
52
53
  const CONFIG_DIR = ".pi";
53
- const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
54
54
  const SUBAGENT_ORCHESTRATION_SKILL = "pi-subagents";
55
55
 
56
56
  const SOURCE_PRIORITY: Record<SkillSource, number> = {
@@ -133,10 +133,10 @@ function getGlobalNpmRoot(): string | null {
133
133
  }
134
134
  }
135
135
 
136
- function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
136
+ function collectInstalledPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
137
137
  const dirs: SkillSearchPath[] = [
138
138
  { path: path.join(cwd, CONFIG_DIR, "npm", "node_modules"), source: "project-package" },
139
- { path: path.join(AGENT_DIR, "npm", "node_modules"), source: "user-package" },
139
+ { path: path.join(agentDir, "npm", "node_modules"), source: "user-package" },
140
140
  ];
141
141
 
142
142
  const globalRoot = getGlobalNpmRoot();
@@ -184,11 +184,11 @@ function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
184
184
  return results;
185
185
  }
186
186
 
187
- function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
187
+ function collectSettingsSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
188
188
  const results: SkillSearchPath[] = [];
189
189
  const settingsFiles = [
190
190
  { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-settings" as const },
191
- { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR, source: "user-settings" as const },
191
+ { file: path.join(agentDir, "settings.json"), base: agentDir, source: "user-settings" as const },
192
192
  ];
193
193
 
194
194
  for (const { file, base, source } of settingsFiles) {
@@ -285,10 +285,10 @@ function resolveSettingsPackageRoot(source: string, baseDir: string): string | u
285
285
  return undefined;
286
286
  }
287
287
 
288
- function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
288
+ function collectSettingsPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
289
289
  const settingsFiles = [
290
290
  { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-package" as const },
291
- { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR, source: "user-package" as const },
291
+ { file: path.join(agentDir, "settings.json"), base: agentDir, source: "user-package" as const },
292
292
  ];
293
293
  const results: SkillSearchPath[] = [];
294
294
 
@@ -315,16 +315,16 @@ function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
315
315
  return results;
316
316
  }
317
317
 
318
- function buildSkillPaths(cwd: string): SkillSearchPath[] {
318
+ function buildSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
319
319
  const skillPaths: SkillSearchPath[] = [
320
320
  { path: path.join(cwd, CONFIG_DIR, "skills"), source: "project" },
321
321
  { path: path.join(cwd, ".agents", "skills"), source: "project" },
322
- { path: path.join(AGENT_DIR, "skills"), source: "user" },
322
+ { path: path.join(agentDir, "skills"), source: "user" },
323
323
  { path: path.join(os.homedir(), ".agents", "skills"), source: "user" },
324
- ...collectInstalledPackageSkillPaths(cwd),
325
- ...collectSettingsPackageSkillPaths(cwd),
324
+ ...collectInstalledPackageSkillPaths(cwd, agentDir),
325
+ ...collectSettingsPackageSkillPaths(cwd, agentDir),
326
326
  ...extractSkillPathsFromPackageRoot(cwd, "project-package"),
327
- ...collectSettingsSkillPaths(cwd),
327
+ ...collectSettingsSkillPaths(cwd, agentDir),
328
328
  ];
329
329
 
330
330
  const deduped = new Map<string, SkillSearchPath>();
@@ -337,15 +337,16 @@ function buildSkillPaths(cwd: string): SkillSearchPath[] {
337
337
  return [...deduped.values()];
338
338
  }
339
339
 
340
- function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSource): SkillSource {
340
+ function inferSkillSource(filePath: string, cwd: string, agentDir: string, sourceHint?: SkillSource): SkillSource {
341
341
  if (sourceHint) return sourceHint;
342
342
 
343
343
  const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
344
344
  const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
345
345
  const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
346
346
  const projectAgentsRoot = path.resolve(cwd, ".agents");
347
- const userSkillsRoot = path.resolve(AGENT_DIR, "skills");
348
- const userPackagesRoot = path.resolve(AGENT_DIR, "npm", "node_modules");
347
+ const userSkillsRoot = path.resolve(agentDir, "skills");
348
+ const userPackagesRoot = path.resolve(agentDir, "npm", "node_modules");
349
+ const userAgentRoot = path.resolve(agentDir);
349
350
  const userAgentsRoot = path.resolve(os.homedir(), ".agents");
350
351
 
351
352
  if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
@@ -354,7 +355,7 @@ function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSourc
354
355
 
355
356
  if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
356
357
  if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
357
- if (isWithinPath(filePath, AGENT_DIR)) return "user-settings";
358
+ if (isWithinPath(filePath, userAgentRoot)) return "user-settings";
358
359
 
359
360
  const globalRoot = getGlobalNpmRoot();
360
361
  if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
@@ -390,7 +391,7 @@ function maybeReadSkillDescription(filePath: string): string | undefined {
390
391
  }
391
392
  }
392
393
 
393
- function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
394
+ function collectFilesystemSkills(cwd: string, agentDir: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
394
395
  const entries: CachedSkillEntry[] = [];
395
396
  const seen = new Set<string>();
396
397
  let order = 0;
@@ -403,7 +404,7 @@ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): Ca
403
404
  entries.push({
404
405
  name,
405
406
  filePath: resolvedFile,
406
- source: inferSkillSource(resolvedFile, cwd, sourceHint),
407
+ source: inferSkillSource(resolvedFile, cwd, agentDir, sourceHint),
407
408
  description: maybeReadSkillDescription(resolvedFile),
408
409
  order: order++,
409
410
  });
@@ -464,12 +465,13 @@ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): Ca
464
465
 
465
466
  function getCachedSkills(cwd: string): CachedSkillEntry[] {
466
467
  const now = Date.now();
467
- if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
468
+ const agentDir = getAgentDir();
469
+ if (loadSkillsCache && loadSkillsCache.cwd === cwd && loadSkillsCache.agentDir === agentDir && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
468
470
  return loadSkillsCache.skills;
469
471
  }
470
472
 
471
- const skillPaths = buildSkillPaths(cwd);
472
- const loaded = collectFilesystemSkills(cwd, skillPaths);
473
+ const skillPaths = buildSkillPaths(cwd, agentDir);
474
+ const loaded = collectFilesystemSkills(cwd, agentDir, skillPaths);
473
475
  const dedupedByName = new Map<string, CachedSkillEntry>();
474
476
 
475
477
  for (const entry of loaded) {
@@ -478,7 +480,7 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
478
480
  }
479
481
 
480
482
  const skills = [...dedupedByName.values()].sort((a, b) => a.order - b.order);
481
- loadSkillsCache = { cwd, skills, timestamp: now };
483
+ loadSkillsCache = { cwd, agentDir, skills, timestamp: now };
482
484
  return skills;
483
485
  }
484
486
 
@@ -0,0 +1,16 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ExtensionConfig } from "../shared/types.ts";
4
+ import { getAgentDir } from "../shared/utils.ts";
5
+
6
+ export function loadConfig(): ExtensionConfig {
7
+ const configPath = path.join(getAgentDir(), "extensions", "subagent", "config.json");
8
+ try {
9
+ if (fs.existsSync(configPath)) {
10
+ return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
11
+ }
12
+ } catch (error) {
13
+ console.error(`Failed to load subagent config from '${configPath}':`, error);
14
+ }
15
+ return {};
16
+ }
@@ -22,7 +22,7 @@ import { discoverAgents } from "../agents/agents.ts";
22
22
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
23
23
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
24
24
  import { cleanupOldChainDirs } from "../shared/settings.ts";
25
- import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, syncResultAnimation } from "../tui/render.ts";
25
+ import { clearLegacyResultAnimationTimer, renderWidget, renderSubagentResult } from "../tui/render.ts";
26
26
  import { SubagentParams } from "./schemas.ts";
27
27
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
28
28
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
@@ -35,9 +35,9 @@ import { inspectSubagentStatus } from "../runs/background/run-status.ts";
35
35
  import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
36
36
  import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
37
37
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
38
+ import { loadConfig } from "./config.ts";
38
39
  import {
39
40
  type Details,
40
- type ExtensionConfig,
41
41
  type SubagentState,
42
42
  ASYNC_DIR,
43
43
  DEFAULT_ARTIFACT_CONFIG,
@@ -56,6 +56,8 @@ import {
56
56
  type SubagentControlMessageDetails,
57
57
  } from "./control-notices.ts";
58
58
 
59
+ export { loadConfig } from "./config.ts";
60
+
59
61
  /**
60
62
  * Derive subagent session base directory from parent session file.
61
63
  * If parent session is ~/.pi/agent/sessions/abc123.jsonl,
@@ -72,18 +74,6 @@ function getSubagentSessionRoot(parentSessionFile: string | null): string {
72
74
  return fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
73
75
  }
74
76
 
75
- function loadConfig(): ExtensionConfig {
76
- const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");
77
- try {
78
- if (fs.existsSync(configPath)) {
79
- return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
80
- }
81
- } catch (error) {
82
- console.error(`Failed to load subagent config from '${configPath}':`, error);
83
- }
84
- return {};
85
- }
86
-
87
77
  function expandTilde(p: string): string {
88
78
  return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
89
79
  }
@@ -142,14 +132,11 @@ function createSlashResultComponent(
142
132
  details: SlashMessageDetails,
143
133
  options: { expanded: boolean },
144
134
  theme: ExtensionContext["ui"]["theme"],
145
- requestRender: () => void,
146
135
  ): Container {
147
136
  const container = new Container();
148
- const animationState: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> } = {};
149
137
  let lastVersion = -1;
150
138
  container.render = (width: number): string[] => {
151
139
  const snapshot = getSlashRenderableSnapshot(details);
152
- syncResultAnimation(snapshot.result, { state: animationState, invalidate: requestRender });
153
140
  if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
154
141
  lastVersion = snapshot.version;
155
142
  rebuildSlashResultContainer(container, snapshot.result, options, theme);
@@ -271,8 +258,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
271
258
  primeExistingResults();
272
259
 
273
260
  const runtimeCleanup = () => {
274
- stopWidgetAnimation();
275
- stopResultAnimations();
276
261
  stopResultWatcher();
277
262
  clearPendingForegroundControlNotices(state);
278
263
  if (state.poller) {
@@ -297,7 +282,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
297
282
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
298
283
  const details = resolveSlashMessageDetails(message.details);
299
284
  if (!details) return undefined;
300
- return createSlashResultComponent(details, options, theme, () => state.lastUiContext?.ui.requestRender?.());
285
+ return createSlashResultComponent(details, options, theme);
301
286
  });
302
287
 
303
288
  pi.registerMessageRenderer<SubagentNotifyDetails>("subagent-notify", (message, options, theme) => {
@@ -466,7 +451,7 @@ DIAGNOSTICS:
466
451
  },
467
452
 
468
453
  renderResult(result, options, theme, context) {
469
- syncResultAnimation(result, context);
454
+ clearLegacyResultAnimationTimer(context);
470
455
  return renderSubagentResult(result, options, theme);
471
456
  },
472
457
 
@@ -514,6 +499,7 @@ DIAGNOSTICS:
514
499
  state.lastUiContext = ctx;
515
500
  if (state.asyncJobs.size > 0) {
516
501
  renderWidget(ctx, Array.from(state.asyncJobs.values()));
502
+ ctx.ui.requestRender?.();
517
503
  ensurePoller();
518
504
  }
519
505
  });
@@ -569,8 +555,6 @@ DIAGNOSTICS:
569
555
  slashBridge.dispose();
570
556
  promptTemplateBridge.cancelAll();
571
557
  promptTemplateBridge.dispose();
572
- stopWidgetAnimation();
573
- stopResultAnimations();
574
558
  if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
575
559
  delete globalStore[runtimeCleanupStoreKey];
576
560
  }
@@ -4,12 +4,13 @@ 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
8
 
8
9
  const PI_INTERCOM_PACKAGE_NAME = "pi-intercom";
9
10
  const CONFIG_DIR = ".pi";
10
11
 
11
12
  function defaultAgentDir(): string {
12
- return path.join(os.homedir(), ".pi", "agent");
13
+ return getAgentDir();
13
14
  }
14
15
 
15
16
  function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
@@ -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, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
14
+ import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
15
15
  import { buildChainInstructions, 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";
@@ -126,7 +126,7 @@ interface AsyncSingleParams {
126
126
  sessionRoot?: string;
127
127
  sessionFile?: string;
128
128
  skills?: string[];
129
- output?: string | false;
129
+ output?: string | boolean;
130
130
  outputMode?: "inline" | "file-only";
131
131
  modelOverride?: string;
132
132
  availableModels?: AvailableModelInfo[];
@@ -323,6 +323,7 @@ export function executeAsyncChain(
323
323
  tools: a.tools,
324
324
  extensions: a.extensions,
325
325
  mcpDirectTools: a.mcpDirectTools,
326
+ completionGuard: a.completionGuard,
326
327
  systemPrompt,
327
328
  systemPromptMode: a.systemPromptMode,
328
329
  inheritProjectContext: a.inheritProjectContext,
@@ -523,7 +524,8 @@ export function executeAsyncSingle(
523
524
  };
524
525
  }
525
526
 
526
- const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, runnerCwd);
527
+ const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
528
+ const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd);
527
529
  const outputMode = params.outputMode ?? "inline";
528
530
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
529
531
  if (validationError) return formatAsyncStartError("single", validationError);
@@ -550,6 +552,7 @@ export function executeAsyncSingle(
550
552
  tools: agentConfig.tools,
551
553
  extensions: agentConfig.extensions,
552
554
  mcpDirectTools: agentConfig.mcpDirectTools,
555
+ completionGuard: agentConfig.completionGuard,
553
556
  systemPrompt,
554
557
  systemPromptMode: agentConfig.systemPromptMode,
555
558
  inheritProjectContext: agentConfig.inheritProjectContext,