pi-subagents 0.8.2 → 0.8.3

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/schemas.ts CHANGED
@@ -11,6 +11,7 @@ export const TaskItem = Type.Object({
11
11
  agent: Type.String(),
12
12
  task: Type.String(),
13
13
  cwd: Type.Optional(Type.String()),
14
+ model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
14
15
  skill: Type.Optional(SkillOverride),
15
16
  });
16
17
 
@@ -71,13 +72,13 @@ export const SubagentParams = Type.Object({
71
72
  })),
72
73
  // Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
73
74
  config: Type.Optional(Type.Any({
74
- description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, model, tools (comma-separated), skills (comma-separated), thinking, output, reads, progress. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent."
75
+ description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent."
75
76
  })),
76
77
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
77
78
  chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
78
- chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: /tmp/pi-chain-runs/ (auto-cleaned after 24h)" })),
79
+ chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: <tmpdir>/pi-chain-runs/ (auto-cleaned after 24h)" })),
79
80
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
80
- agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
81
+ agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
81
82
  cwd: Type.Optional(Type.String()),
82
83
  maxOutput: MaxOutputSchema,
83
84
  artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
@@ -89,7 +90,7 @@ export const SubagentParams = Type.Object({
89
90
  // Clarification TUI
90
91
  clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution (default: true for chains, false for single/parallel). Implies sync mode." })),
91
92
  // Solo agent overrides
92
- output: Type.Optional(Type.Any({ description: "Override output file for single agent (string), or false to disable (uses agent default if omitted)" })),
93
+ output: Type.Optional(Type.Any({ description: "Override output file for single agent (string), or false to disable (uses agent default if omitted). Absolute paths are used as-is; relative paths resolve against cwd." })),
93
94
  skill: Type.Optional(SkillOverride),
94
95
  model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
95
96
  });
package/settings.ts CHANGED
@@ -3,11 +3,12 @@
3
3
  */
4
4
 
5
5
  import * as fs from "node:fs";
6
+ import * as os from "node:os";
6
7
  import * as path from "node:path";
7
8
  import type { AgentConfig } from "./agents.js";
8
9
  import { normalizeSkillInput } from "./skills.js";
9
10
 
10
- const CHAIN_RUNS_DIR = "/tmp/pi-chain-runs";
11
+ const CHAIN_RUNS_DIR = path.join(os.tmpdir(), "pi-chain-runs");
11
12
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
12
13
 
13
14
  // =============================================================================
@@ -358,6 +359,8 @@ export interface ParallelTaskResult {
358
359
  output: string;
359
360
  exitCode: number;
360
361
  error?: string;
362
+ outputTargetPath?: string;
363
+ outputTargetExists?: boolean;
361
364
  }
362
365
 
363
366
  /**
@@ -368,7 +371,20 @@ export function aggregateParallelOutputs(results: ParallelTaskResult[]): string
368
371
  return results
369
372
  .map((r, i) => {
370
373
  const header = `=== Parallel Task ${i + 1} (${r.agent}) ===`;
371
- return `${header}\n${r.output}`;
374
+ const hasTextOutput = Boolean(r.output?.trim());
375
+ const status = r.exitCode !== 0
376
+ ? `⚠️ FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
377
+ : r.error
378
+ ? `⚠️ WARNING: ${r.error}`
379
+ : !hasTextOutput && r.outputTargetPath && r.outputTargetExists === false
380
+ ? `⚠️ EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
381
+ : !hasTextOutput && !r.outputTargetPath
382
+ ? "⚠️ EMPTY OUTPUT (no textual response returned)"
383
+ : "";
384
+ const body = status
385
+ ? (hasTextOutput ? `${status}\n${r.output}` : status)
386
+ : r.output;
387
+ return `${header}\n${body}`;
372
388
  })
373
389
  .join("\n\n");
374
390
  }
@@ -0,0 +1,55 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export function resolveSingleOutputPath(
5
+ output: string | false | undefined,
6
+ runtimeCwd: string,
7
+ requestedCwd?: string,
8
+ ): string | undefined {
9
+ if (typeof output !== "string" || !output) return undefined;
10
+ if (path.isAbsolute(output)) return output;
11
+ const baseCwd = requestedCwd
12
+ ? (path.isAbsolute(requestedCwd) ? requestedCwd : path.resolve(runtimeCwd, requestedCwd))
13
+ : runtimeCwd;
14
+ return path.resolve(baseCwd, output);
15
+ }
16
+
17
+ export function injectSingleOutputInstruction(task: string, outputPath: string | undefined): string {
18
+ if (!outputPath) return task;
19
+ return `${task}\n\n---\n**Output:** Write your findings to: ${outputPath}`;
20
+ }
21
+
22
+ export function persistSingleOutput(
23
+ outputPath: string | undefined,
24
+ fullOutput: string,
25
+ ): { savedPath?: string; error?: string } {
26
+ if (!outputPath) return {};
27
+ try {
28
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
29
+ fs.writeFileSync(outputPath, fullOutput, "utf-8");
30
+ return { savedPath: outputPath };
31
+ } catch (err) {
32
+ return { error: err instanceof Error ? err.message : String(err) };
33
+ }
34
+ }
35
+
36
+ export function finalizeSingleOutput(params: {
37
+ fullOutput: string;
38
+ truncatedOutput?: string;
39
+ outputPath?: string;
40
+ exitCode: number;
41
+ }): { displayOutput: string; savedPath?: string; saveError?: string } {
42
+ let displayOutput = params.truncatedOutput || params.fullOutput;
43
+ if (params.outputPath && params.exitCode === 0) {
44
+ const save = persistSingleOutput(params.outputPath, params.fullOutput);
45
+ if (save.savedPath) {
46
+ displayOutput += `\n\n📄 Output saved to: ${save.savedPath}`;
47
+ return { displayOutput, savedPath: save.savedPath };
48
+ }
49
+ if (save.error) {
50
+ displayOutput += `\n\n⚠️ Failed to save output to: ${params.outputPath}\n${save.error}`;
51
+ return { displayOutput, saveError: save.error };
52
+ }
53
+ }
54
+ return { displayOutput };
55
+ }
package/skills.ts CHANGED
@@ -5,12 +5,24 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
+ import { loadSkills, type Skill } from "@mariozechner/pi-coding-agent";
9
+
10
+ export type SkillSource =
11
+ | "project"
12
+ | "user"
13
+ | "project-package"
14
+ | "user-package"
15
+ | "project-settings"
16
+ | "user-settings"
17
+ | "extension"
18
+ | "builtin"
19
+ | "unknown";
8
20
 
9
21
  export interface ResolvedSkill {
10
22
  name: string;
11
23
  path: string;
12
24
  content: string;
13
- source: "project" | "user";
25
+ source: SkillSource;
14
26
  }
15
27
 
16
28
  interface SkillCacheEntry {
@@ -18,9 +30,35 @@ interface SkillCacheEntry {
18
30
  skill: ResolvedSkill;
19
31
  }
20
32
 
33
+ interface CachedSkillEntry {
34
+ name: string;
35
+ filePath: string;
36
+ source: SkillSource;
37
+ description?: string;
38
+ order: number;
39
+ }
40
+
21
41
  const skillCache = new Map<string, SkillCacheEntry>();
22
42
  const MAX_CACHE_SIZE = 50;
23
43
 
44
+ let loadSkillsCache: { cwd: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
45
+ const LOAD_SKILLS_CACHE_TTL_MS = 5000;
46
+
47
+ const CONFIG_DIR = ".pi";
48
+ const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
49
+
50
+ const SOURCE_PRIORITY: Record<SkillSource, number> = {
51
+ project: 700,
52
+ "project-settings": 650,
53
+ "project-package": 600,
54
+ user: 300,
55
+ "user-settings": 250,
56
+ "user-package": 200,
57
+ extension: 150,
58
+ builtin: 100,
59
+ unknown: 0,
60
+ };
61
+
24
62
  function stripSkillFrontmatter(content: string): string {
25
63
  const normalized = content.replace(/\r\n/g, "\n");
26
64
  if (!normalized.startsWith("---")) return normalized;
@@ -31,27 +69,187 @@ function stripSkillFrontmatter(content: string): string {
31
69
  return normalized.slice(endIndex + 4).trim();
32
70
  }
33
71
 
34
- export function resolveSkillPath(
35
- skillName: string,
36
- cwd: string,
37
- ): { path: string; source: "project" | "user" } | undefined {
38
- const projectPath = path.resolve(cwd, ".pi", "skills", skillName, "SKILL.md");
39
- if (fs.existsSync(projectPath)) {
40
- return { path: projectPath, source: "project" };
72
+ function isWithinPath(filePath: string, dir: string): boolean {
73
+ const relative = path.relative(dir, filePath);
74
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
75
+ }
76
+
77
+ function getPackageSkillPaths(packageRoot: string): string[] {
78
+ const pkgJsonPath = path.join(packageRoot, "package.json");
79
+ try {
80
+ const content = fs.readFileSync(pkgJsonPath, "utf-8");
81
+ const pkg = JSON.parse(content);
82
+ const piSkills = pkg?.pi?.skills;
83
+ if (!Array.isArray(piSkills)) return [];
84
+ return piSkills
85
+ .filter((s: unknown) => typeof s === "string")
86
+ .map((s: string) => path.resolve(packageRoot, s));
87
+ } catch {
88
+ return [];
41
89
  }
90
+ }
91
+
92
+ function collectPackageSkillPaths(cwd: string): string[] {
93
+ const dirs = [
94
+ path.join(cwd, CONFIG_DIR, "npm", "node_modules"),
95
+ path.join(AGENT_DIR, "npm", "node_modules"),
96
+ ];
97
+ const results: string[] = [];
42
98
 
43
- const userPath = path.join(os.homedir(), ".pi", "agent", "skills", skillName, "SKILL.md");
44
- if (fs.existsSync(userPath)) {
45
- return { path: userPath, source: "user" };
99
+ for (const dir of dirs) {
100
+ if (!fs.existsSync(dir)) continue;
101
+ let entries: fs.Dirent[];
102
+ try {
103
+ entries = fs.readdirSync(dir, { withFileTypes: true });
104
+ } catch {
105
+ continue;
106
+ }
107
+
108
+ for (const entry of entries) {
109
+ if (entry.name.startsWith(".")) continue;
110
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
111
+
112
+ if (entry.name.startsWith("@")) {
113
+ const scopeDir = path.join(dir, entry.name);
114
+ let scopeEntries: fs.Dirent[];
115
+ try {
116
+ scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
117
+ } catch {
118
+ continue;
119
+ }
120
+ for (const scopeEntry of scopeEntries) {
121
+ if (scopeEntry.name.startsWith(".")) continue;
122
+ if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink()) continue;
123
+ const pkgRoot = path.join(scopeDir, scopeEntry.name);
124
+ results.push(...getPackageSkillPaths(pkgRoot));
125
+ }
126
+ continue;
127
+ }
128
+
129
+ const pkgRoot = path.join(dir, entry.name);
130
+ results.push(...getPackageSkillPaths(pkgRoot));
131
+ }
46
132
  }
47
133
 
48
- return undefined;
134
+ return results;
135
+ }
136
+
137
+ function collectSettingsSkillPaths(cwd: string): string[] {
138
+ const results: string[] = [];
139
+ const settingsFiles = [
140
+ { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR) },
141
+ { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR },
142
+ ];
143
+
144
+ for (const { file, base } of settingsFiles) {
145
+ try {
146
+ const content = fs.readFileSync(file, "utf-8");
147
+ const settings = JSON.parse(content);
148
+ const skills = settings?.skills;
149
+ if (!Array.isArray(skills)) continue;
150
+ for (const entry of skills) {
151
+ if (typeof entry !== "string") continue;
152
+ let resolved = entry;
153
+ if (resolved.startsWith("~/")) {
154
+ resolved = path.join(os.homedir(), resolved.slice(2));
155
+ } else if (!path.isAbsolute(resolved)) {
156
+ resolved = path.resolve(base, resolved);
157
+ }
158
+ results.push(resolved);
159
+ }
160
+ } catch {}
161
+ }
162
+
163
+ return results;
164
+ }
165
+
166
+ function buildSkillPaths(cwd: string): string[] {
167
+ const defaultSkillPaths = [
168
+ path.join(cwd, CONFIG_DIR, "skills"),
169
+ path.join(AGENT_DIR, "skills"),
170
+ ];
171
+ const packagePaths = collectPackageSkillPaths(cwd);
172
+ const settingsPaths = collectSettingsSkillPaths(cwd);
173
+ return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
174
+ }
175
+
176
+ function inferSkillSource(rawSource: unknown, filePath: string, cwd: string): SkillSource {
177
+ const source = typeof rawSource === "string" ? rawSource : "";
178
+ const projectRoot = path.resolve(cwd, CONFIG_DIR);
179
+ const isProjectScoped = isWithinPath(filePath, projectRoot);
180
+ const isUserScoped = isWithinPath(filePath, AGENT_DIR);
181
+
182
+ if (source === "project") return "project";
183
+ if (source === "user") return "user";
184
+ if (source === "settings") {
185
+ if (isProjectScoped) return "project-settings";
186
+ if (isUserScoped) return "user-settings";
187
+ return "unknown";
188
+ }
189
+ if (source === "package") {
190
+ if (isProjectScoped) return "project-package";
191
+ if (isUserScoped) return "user-package";
192
+ return "unknown";
193
+ }
194
+ if (source === "extension") return "extension";
195
+ if (source === "builtin") return "builtin";
196
+
197
+ if (isProjectScoped) return "project";
198
+ if (isUserScoped) return "user";
199
+ return "unknown";
200
+ }
201
+
202
+ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candidate: CachedSkillEntry): CachedSkillEntry {
203
+ if (!existing) return candidate;
204
+ const existingPriority = SOURCE_PRIORITY[existing.source] ?? 0;
205
+ const candidatePriority = SOURCE_PRIORITY[candidate.source] ?? 0;
206
+ if (candidatePriority > existingPriority) return candidate;
207
+ if (candidatePriority < existingPriority) return existing;
208
+ return candidate.order < existing.order ? candidate : existing;
209
+ }
210
+
211
+ function getCachedSkills(cwd: string): CachedSkillEntry[] {
212
+ const now = Date.now();
213
+ if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
214
+ return loadSkillsCache.skills;
215
+ }
216
+
217
+ const skillPaths = buildSkillPaths(cwd);
218
+ const loaded = loadSkills({ cwd, skillPaths, includeDefaults: false });
219
+ const dedupedByName = new Map<string, CachedSkillEntry>();
220
+
221
+ for (let i = 0; i < loaded.skills.length; i++) {
222
+ const skill = loaded.skills[i] as Skill;
223
+ const entry: CachedSkillEntry = {
224
+ name: skill.name,
225
+ filePath: skill.filePath,
226
+ source: inferSkillSource((skill as { source?: unknown }).source, skill.filePath, cwd),
227
+ description: skill.description,
228
+ order: i,
229
+ };
230
+ const current = dedupedByName.get(entry.name);
231
+ dedupedByName.set(entry.name, chooseHigherPrioritySkill(current, entry));
232
+ }
233
+
234
+ const skills = [...dedupedByName.values()].sort((a, b) => a.order - b.order);
235
+ loadSkillsCache = { cwd, skills, timestamp: now };
236
+ return skills;
237
+ }
238
+
239
+ export function resolveSkillPath(
240
+ skillName: string,
241
+ cwd: string,
242
+ ): { path: string; source: SkillSource } | undefined {
243
+ const skills = getCachedSkills(cwd);
244
+ const skill = skills.find((s) => s.name === skillName);
245
+ if (!skill) return undefined;
246
+ return { path: skill.filePath, source: skill.source };
49
247
  }
50
248
 
51
249
  export function readSkill(
52
250
  skillName: string,
53
251
  skillPath: string,
54
- source: "project" | "user",
252
+ source: SkillSource,
55
253
  ): ResolvedSkill | undefined {
56
254
  try {
57
255
  const stat = fs.statSync(skillPath);
@@ -123,84 +321,27 @@ export function normalizeSkillInput(
123
321
  if (input === false) return false;
124
322
  if (input === true || input === undefined) return undefined;
125
323
  if (Array.isArray(input)) {
126
- // Deduplicate while preserving order
127
324
  return [...new Set(input.map((s) => s.trim()).filter((s) => s.length > 0))];
128
325
  }
129
- // Deduplicate while preserving order
130
326
  return [...new Set(input.split(",").map((s) => s.trim()).filter((s) => s.length > 0))];
131
327
  }
132
328
 
133
- function isDirectory(p: string): boolean {
134
- try {
135
- return fs.statSync(p).isDirectory();
136
- } catch {
137
- return false;
138
- }
139
- }
140
-
141
329
  export function discoverAvailableSkills(cwd: string): Array<{
142
330
  name: string;
143
- source: "project" | "user";
331
+ source: SkillSource;
144
332
  description?: string;
145
333
  }> {
146
- const skills: Array<{ name: string; source: "project" | "user"; description?: string }> = [];
147
- const seen = new Set<string>();
148
-
149
- const scanDir = (dir: string, source: "project" | "user") => {
150
- if (!fs.existsSync(dir)) return;
151
-
152
- try {
153
- const entries = fs.readdirSync(dir, { withFileTypes: true });
154
- for (const entry of entries) {
155
- const fullPath = path.join(dir, entry.name);
156
-
157
- const isDir = entry.isDirectory() || (entry.isSymbolicLink() && isDirectory(fullPath));
158
- if (!isDir) continue;
159
-
160
- const skillPath = path.join(fullPath, "SKILL.md");
161
- if (!fs.existsSync(skillPath)) continue;
162
-
163
- if (source === "project" || !seen.has(entry.name)) {
164
- let description: string | undefined;
165
- try {
166
- const content = fs.readFileSync(skillPath, "utf-8");
167
- if (content.startsWith("---")) {
168
- const endIndex = content.indexOf("\n---", 3);
169
- if (endIndex !== -1) {
170
- const fmBlock = content.slice(0, endIndex);
171
- const match = fmBlock.match(/description:\s*(.+)/);
172
- if (match) {
173
- let desc = match[1].trim();
174
- if (
175
- (desc.startsWith("\"") && desc.endsWith("\"")) ||
176
- (desc.startsWith("'") && desc.endsWith("'"))
177
- ) {
178
- desc = desc.slice(1, -1);
179
- }
180
- description = desc;
181
- }
182
- }
183
- }
184
- } catch {}
185
-
186
- if (source === "project" && seen.has(entry.name)) {
187
- const idx = skills.findIndex((s) => s.name === entry.name);
188
- if (idx !== -1) skills.splice(idx, 1);
189
- }
190
-
191
- skills.push({ name: entry.name, source, description });
192
- seen.add(entry.name);
193
- }
194
- }
195
- } catch {}
196
- };
197
-
198
- scanDir(path.join(os.homedir(), ".pi", "agent", "skills"), "user");
199
- scanDir(path.resolve(cwd, ".pi", "skills"), "project");
200
-
201
- return skills.sort((a, b) => a.name.localeCompare(b.name));
334
+ const skills = getCachedSkills(cwd);
335
+ return skills
336
+ .map((s) => ({
337
+ name: s.name,
338
+ source: s.source,
339
+ description: s.description,
340
+ }))
341
+ .sort((a, b) => a.name.localeCompare(b.name));
202
342
  }
203
343
 
204
344
  export function clearSkillCache(): void {
205
345
  skillCache.clear();
346
+ loadSkillsCache = null;
206
347
  }
@@ -5,6 +5,8 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { appendJsonl, getArtifactPaths } from "./artifacts.js";
8
+ import { getPiSpawnCommand } from "./pi-spawn.js";
9
+ import { persistSingleOutput } from "./single-output.js";
8
10
  import {
9
11
  type ArtifactConfig,
10
12
  type ArtifactPaths,
@@ -20,9 +22,11 @@ interface SubagentStep {
20
22
  cwd?: string;
21
23
  model?: string;
22
24
  tools?: string[];
25
+ extensions?: string[];
23
26
  mcpDirectTools?: string[];
24
27
  systemPrompt?: string | null;
25
28
  skills?: string[];
29
+ outputPath?: string;
26
30
  }
27
31
 
28
32
  interface SubagentRunConfig {
@@ -104,7 +108,8 @@ function runPiStreaming(
104
108
  return new Promise((resolve) => {
105
109
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
106
110
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv() };
107
- const child = spawn("pi", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
111
+ const spawnSpec = getPiSpawnCommand(args);
112
+ const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
108
113
  let stdout = "";
109
114
 
110
115
  child.stdout.on("data", (chunk: Buffer) => {
@@ -337,19 +342,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
337
342
  } catch {}
338
343
  args.push("--session-dir", config.sessionDir);
339
344
  }
340
- if (step.model) args.push("--model", step.model);
345
+ // Use --models (not --model) because pi CLI silently ignores --model
346
+ // without a companion --provider flag. --models resolves the provider
347
+ // automatically via resolveModelScope. See: #8
348
+ if (step.model) args.push("--models", step.model);
349
+ const toolExtensionPaths: string[] = [];
341
350
  if (step.tools?.length) {
342
351
  const builtinTools: string[] = [];
343
- const extensionPaths: string[] = [];
344
352
  for (const tool of step.tools) {
345
353
  if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
346
- extensionPaths.push(tool);
354
+ toolExtensionPaths.push(tool);
347
355
  } else {
348
356
  builtinTools.push(tool);
349
357
  }
350
358
  }
351
359
  if (builtinTools.length > 0) args.push("--tools", builtinTools.join(","));
352
- for (const extPath of extensionPaths) args.push("--extension", extPath);
360
+ }
361
+ if (step.extensions !== undefined) {
362
+ args.push("--no-extensions");
363
+ for (const extPath of step.extensions) args.push("--extension", extPath);
364
+ } else {
365
+ for (const extPath of toolExtensionPaths) args.push("--extension", extPath);
353
366
  }
354
367
 
355
368
  let tmpDir: string | null = null;
@@ -392,6 +405,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
392
405
 
393
406
  const output = (result.stdout || "").trim();
394
407
  previousOutput = output;
408
+ let outputForSummary = output;
409
+ if (step.outputPath && result.exitCode === 0) {
410
+ const persisted = persistSingleOutput(step.outputPath, output);
411
+ if (persisted.savedPath) {
412
+ outputForSummary = output
413
+ ? `${output}\n\n📄 Output saved to: ${persisted.savedPath}`
414
+ : `📄 Output saved to: ${persisted.savedPath}`;
415
+ } else if (persisted.error) {
416
+ outputForSummary = output
417
+ ? `${output}\n\n⚠️ Failed to save output to: ${step.outputPath}\n${persisted.error}`
418
+ : `⚠️ Failed to save output to: ${step.outputPath}\n${persisted.error}`;
419
+ }
420
+ }
395
421
 
396
422
  const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
397
423
  const stepTokens: TokenUsage | null = cumulativeTokens
@@ -407,7 +433,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
407
433
 
408
434
  const stepResult: StepResult = {
409
435
  agent: step.agent,
410
- output,
436
+ output: outputForSummary,
411
437
  success: result.exitCode === 0,
412
438
  artifactPaths,
413
439
  };
package/types.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  * Type definitions for the subagent extension
3
3
  */
4
4
 
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
5
7
  import type { Message } from "@mariozechner/pi-ai";
6
8
 
7
9
  // ============================================================================
@@ -237,8 +239,8 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
237
239
 
238
240
  export const MAX_PARALLEL = 8;
239
241
  export const MAX_CONCURRENCY = 4;
240
- export const RESULTS_DIR = "/tmp/pi-async-subagent-results";
241
- export const ASYNC_DIR = "/tmp/pi-async-subagent-runs";
242
+ export const RESULTS_DIR = path.join(os.tmpdir(), "pi-async-subagent-results");
243
+ export const ASYNC_DIR = path.join(os.tmpdir(), "pi-async-subagent-runs");
242
244
  export const WIDGET_KEY = "subagent-async";
243
245
  export const POLL_INTERVAL_MS = 250;
244
246
  export const MAX_WIDGET_JOBS = 4;